diff --git a/Dockerfile b/Dockerfile
index 191bffe..2111d50 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -14,6 +14,6 @@ RUN apt-get update && \
apt-get purge -y --auto-remove && \
rm -rf /var/lib/apt/lists/*
-COPY ./lib /app/lib
+COPY ./src /app/src
-CMD ["gunicorn", "-c", "lib/settings/gunicorn.py", "-w", "1", "--threads=2", "-k", "uvicorn.workers.UvicornWorker", "lib.api:app", "--log-level", "Debug", "-b", "0.0.0.0:3000", "--timeout", "60"]
+CMD ["gunicorn", "-c", "src/settings/gunicorn.py", "-w", "1", "--threads=2", "-k", "uvicorn.workers.UvicornWorker", "src.api:app", "--log-level", "Debug", "-b", "0.0.0.0:3000", "--timeout", "60", "--loop", "uvloop"]
diff --git a/Makefile b/Makefile
index 815840e..c65bc7f 100644
--- a/Makefile
+++ b/Makefile
@@ -2,26 +2,26 @@ format: black ruff
lint: flake8 pylint
black:
- black ./lib || true
+ black ./src || true
black ./tests || true
flake8:
- flake8 --ignore E501,E402,F401,W503,C0414 ./lib || true
+ flake8 --ignore E501,E402,F401,W503,C0414 ./src || true
flake8 --ignore E501,E402,F401,W503,C0414 ./tests || true
pylint:
- pylint ./lib || true
+ pylint ./src || true
pylint ./tests || true
ruff:
- ruff check --fix ./lib || true
+ ruff check --fix ./src || true
ruff check --fix ./tests || true
test:
python3 -m pytest .
dev:
- python3 -m uvicorn lib:app --reload --port 3000
+ python3 -m uvicorn src:app --reload --port 3000 --loop uvloop
clean:
docker stop infinity-api
diff --git a/README.md b/README.md
index 7513edb..709dc50 100644
--- a/README.md
+++ b/README.md
@@ -25,8 +25,8 @@ $ touch .env && echo MONGODB_CONNECTION_STRING="$ConnectionString" > .env
- run docker compose: `docker-compose up --build -d`
### Standalone
-- Dev: `python3 -m uvicorn lib:app --reload --port 3000`
-- Prod: `gunicorn -k uvicorn.workers.UvicornWorker lib:app -b 0.0.0.0:3000`
+- Dev: `python3 -m uvicorn src:app --reload --port 3000`
+- Prod: `gunicorn -k uvicorn.workers.UvicornWorker src:app -b 0.0.0.0:3000`
## Project structure
```
@@ -39,6 +39,7 @@ $ touch .env && echo MONGODB_CONNECTION_STRING="$ConnectionString" > .env
│ │── secrets.py
│ │
│ ├── controllers
+│ │ ├── interface.py
│ │ ├── environment.py
│ │ ├── flight.py
│ │ ├── motor.py
@@ -57,65 +58,31 @@ $ touch .env && echo MONGODB_CONNECTION_STRING="$ConnectionString" > .env
│ │ └── rocket.py
│ │
│ ├── repositories
-│ │ ├── repo.py
+│ │ ├── interface.py
│ │ ├── environment.py
│ │ ├── flight.py
│ │ ├── motor.py
│ │ └── rocket.py
│ │
│ ├── models
-│ │ ├── aerosurfaces.py
+│ │ ├── interface.py
│ │ ├── environment.py
│ │ ├── flight.py
│ │ ├── motor.py
-│ │ └── rocket.py
+│ │ ├── rocket.py
+│ │ │
+│ │ └── sub
+│ │ ├── aerosurfaces.py
+│ │ └── tanks.py
│ │
│ └── views
+│ ├── interface.py
│ ├── environment.py
│ ├── flight.py
│ ├── motor.py
│ └── rocket.py
│
└── tests
- ├── integration
- │ ├── test_environment_integration.py
- │ ├── test_motor_integration.py
- │ ├── test_rocket_integration.py
- │ └── test_flight_integration.py
- │
- └── unit
- ├── test_secrets.py
- ├── test_api.py
- │
- ├── test_controllers
- │ ├── test_environment_controller.py
- │ ├── test_flight_controller.py
- │ ├── test_motor_controller.py
- │ └── test_rocket_controller.py
- │
- ├── test_services
- │ ├── test_environment_service.py
- │ ├── test_flight_service.py
- │ ├── test_motor_service.py
- │ └── test_rocket_serice.py
- │
- ├── test_repositories
- │ ├── test_environment_repo.py
- │ ├── test_flight_repo.py
- │ ├── test_motor_repo.py
- │ └── test_rocket_repo.py
- │
- ├── test_models
- │ ├── test_environment_model.py
- │ ├── test_flight_model.py
- │ ├── test_motor_model.py
- │ └── test_rocket_model.py
- │
- └── test_views
- ├── test_environment_view.py
- ├── test_flight_view.py
- ├── test_motor_view.py
- └── test_rocket_view.py
```
## DOCS
@@ -135,21 +102,21 @@ sequenceDiagram
User ->> API: POST /model
API ->> MongoDB: Persist API Model as a document
MongoDB -->> API: Model ID
- API -->> User: ModelCreated View
+ API -->> User: 201 ModelCreated View
User ->> API: GET /model/:id
API ->> MongoDB: Read API Model document
MongoDB -->> API: API Model document
- API -->> User: API ModelView
+ API -->> User: 200 API ModelView
User ->> API: PUT /model/:id
API ->> MongoDB: Update API Model document
- API -->> User: ModelUpdated View
+ API -->> User: 204
User ->> API: DELETE /model/:id
API ->> MongoDB: Delete API Model document
MongoDB -->> API: Deletion Confirmation
- API -->> User: ModelDeleted View
+ API -->> User: 204
```
@@ -159,19 +126,19 @@ sequenceDiagram
participant User
participant API
participant MongoDB
- participant Rocketpy lib
+ participant RocketPy lib
- User ->> API: GET /summary/model/:id
- API -->> MongoDB: Retrieve Rocketpy native class
- MongoDB -->> API: Rocketpy native class
- API ->> Rocketpy lib: Simulate Rocketpy native class
- Rocketpy lib -->> API: Simulation Results
+ User ->> API: GET model/:id/simulate/
+ API -->> MongoDB: Retrieve API Model document
+ MongoDB -->> API: API Model document
+ API ->> RocketPy: Initialize RocketPy native class and simulate
+ RocketPy lib -->> API: Simulation Results
API -->> User: Simulation Results
User ->> API: GET /model/:id/rocketpy
- API -->> MongoDB: Retrieve Rocketpy Model
- MongoDB -->> API: Rocketpy Model
- API ->> Rocketpy lib: Rocketpy Model
- Rocketpy lib -->> API: Rocketpy native class
- API -->> User: Rocketpy native class as .dill binary
+ API -->> MongoDB: Retrieve API Model document
+ MongoDB -->> API: API Model document
+ API ->> RocketPy: Initialize RocketPy native class
+ RocketPy lib -->> API: RocketPy native class
+ API -->> User: RocketPy native class as .dill binary (amd64)
```
diff --git a/lib/controllers/__init__.py b/lib/controllers/__init__.py
deleted file mode 100644
index c8add6a..0000000
--- a/lib/controllers/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# lib/controllers/__init__.py
diff --git a/lib/controllers/environment.py b/lib/controllers/environment.py
deleted file mode 100644
index 2da51c2..0000000
--- a/lib/controllers/environment.py
+++ /dev/null
@@ -1,274 +0,0 @@
-from typing import Union
-
-from fastapi import HTTPException, status
-from pymongo.errors import PyMongoError
-
-from lib import logger, parse_error
-from lib.models.environment import Env
-from lib.services.environment import EnvironmentService
-from lib.repositories.environment import EnvRepository
-from lib.views.environment import (
- EnvSummary,
- EnvCreated,
- EnvDeleted,
- EnvUpdated,
-)
-
-
-class EnvController:
- """
- Controller for the Environment model.
-
- Enables:
- - Simulation of a RocketPy Environment from models.Env
- - CRUD operations over models.Env on the database
- """
-
- @staticmethod
- async def create_env(env: Env) -> Union[EnvCreated, HTTPException]:
- """
- Create a env in the database.
-
- Returns:
- views.EnvCreated
- """
- env_repo = None
- try:
- async with EnvRepository(env) as env_repo:
- await env_repo.create_env()
- except PyMongoError as e:
- logger.error(
- f"controllers.environment.create_env: PyMongoError {e}"
- )
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to create environment in db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.environment.create_env: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to create environment: {exc_str}",
- ) from e
- else:
- return EnvCreated(env_id=env_repo.env_id)
- finally:
- env_id = (
- getattr(env_repo, 'env_id', 'unknown')
- if env_repo
- else 'unknown'
- )
- if env_repo:
- logger.info(
- f"Call to controllers.environment.create_env completed for Env {env_id}"
- )
-
- @staticmethod
- async def get_env_by_id(env_id: str) -> Union[Env, HTTPException]:
- """
- Get a env from the database.
-
- Args:
- env_id: str
-
- Returns:
- models.Env
-
- Raises:
- HTTP 404 Not Found: If the env is not found in the database.
- """
- try:
- async with EnvRepository() as env_repo:
- await env_repo.get_env_by_id(env_id)
- env = env_repo.env
- except PyMongoError as e:
- logger.error(
- f"controllers.environment.get_env_by_id: PyMongoError {e}"
- )
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to read environment from db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.environment.get_env_by_id: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to read environment: {exc_str}",
- ) from e
- else:
- if env:
- return env
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail="Environment not found",
- )
- finally:
- logger.info(
- f"Call to controllers.environment.get_env_by_id completed for Env {env_id}"
- )
-
- @classmethod
- async def get_rocketpy_env_binary(
- cls,
- env_id: str,
- ) -> Union[bytes, HTTPException]:
- """
- Get rocketpy.Environmnet dill binary.
-
- Args:
- env_id: str
-
- Returns:
- bytes
-
- Raises:
- HTTP 404 Not Found: If the env is not found in the database.
- """
- try:
- env = await cls.get_env_by_id(env_id)
- env_service = EnvironmentService.from_env_model(env)
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(
- f"controllers.environment.get_rocketpy_env_as_binary: {exc_str}"
- )
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to read environment: {exc_str}",
- ) from e
- else:
- return env_service.get_env_binary()
- finally:
- logger.info(
- f"Call to controllers.environment.get_rocketpy_env_binary completed for Env {env_id}"
- )
-
- @staticmethod
- async def update_env_by_id(
- env_id: str, env: Env
- ) -> Union[EnvUpdated, HTTPException]:
- """
- Update a models.Env in the database.
-
- Args:
- env_id: str
-
- Returns:
- views.EnvUpdated
-
- Raises:
- HTTP 404 Not Found: If the env is not found in the database.
- """
- try:
- async with EnvRepository(env) as env_repo:
- await env_repo.update_env_by_id(env_id)
- except PyMongoError as e:
- logger.error(
- f"controllers.environment.update_env: PyMongoError {e}"
- )
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to update environment from db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.environment.update_env: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to update environment: {exc_str}",
- ) from e
- else:
- return EnvUpdated(env_id=env_id)
- finally:
- logger.info(
- f"Call to controllers.environment.update_env completed for Env {env_id}"
- )
-
- @staticmethod
- async def delete_env_by_id(
- env_id: str,
- ) -> Union[EnvDeleted, HTTPException]:
- """
- Delete a models.Env from the database.
-
- Args:
- env_id: str
-
- Returns:
- views.EnvDeleted
-
- Raises:
- HTTP 404 Not Found: If the env is not found in the database.
- """
- try:
- async with EnvRepository() as env_repo:
- await env_repo.delete_env_by_id(env_id)
- except PyMongoError as e:
- logger.error(
- f"controllers.environment.delete_env: PyMongoError {e}"
- )
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to delete environment from db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.environment.delete_env: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to delete environment: {exc_str}",
- ) from e
- else:
- return EnvDeleted(env_id=env_id)
- finally:
- logger.info(
- f"Call to controllers.environment.delete_env completed for Env {env_id}"
- )
-
- @classmethod
- async def simulate_env(
- cls, env_id: str
- ) -> Union[EnvSummary, HTTPException]:
- """
- Simulate a rocket environment.
-
- Args:
- env_id: str.
-
- Returns:
- EnvSummary
-
- Raises:
- HTTP 404 Not Found: If the env does not exist in the database.
- """
- try:
- env = await cls.get_env_by_id(env_id)
- env_service = EnvironmentService.from_env_model(env)
- env_summary = env_service.get_env_summary()
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.environment.simulate: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to simulate environment, parameters may contain data that is not physically coherent: {exc_str}",
- ) from e
- else:
- return env_summary
- finally:
- logger.info(
- f"Call to controllers.environment.simulate completed for Env {env_id}"
- )
diff --git a/lib/controllers/flight.py b/lib/controllers/flight.py
deleted file mode 100644
index 42fe456..0000000
--- a/lib/controllers/flight.py
+++ /dev/null
@@ -1,394 +0,0 @@
-from typing import Union
-from fastapi import HTTPException, status
-from pymongo.errors import PyMongoError
-
-
-from lib import logger, parse_error
-from lib.controllers.rocket import RocketController
-from lib.models.environment import Env
-from lib.models.rocket import Rocket
-from lib.models.flight import Flight
-from lib.views.motor import MotorView
-from lib.views.rocket import RocketView
-from lib.views.flight import (
- FlightSummary,
- FlightCreated,
- FlightUpdated,
- FlightDeleted,
- FlightView,
-)
-from lib.repositories.flight import FlightRepository
-from lib.services.flight import FlightService
-
-
-class FlightController:
- """
- Controller for the Flight model.
-
- Enables:
- - Create a RocketPyFlight object from a Flight model object.
- - Generate trajectory simulation from a RocketPyFlight object.
- - Create both Flight model and RocketPyFlight objects in the database.
- - Update both Flight model and RocketPyFlight objects in the database.
- - Delete both Flight model and RocketPyFlight objects from the database.
- - Read a Flight model from the database.
- - Read a RocketPyFlight object from the database.
-
- """
-
- @staticmethod
- def guard(flight: Flight):
- RocketController.guard(flight.rocket)
-
- @classmethod
- async def create_flight(
- cls, flight: Flight
- ) -> Union[FlightCreated, HTTPException]:
- """
- Create a flight in the database.
-
- Returns:
- views.FlightCreated
- """
- flight_repo = None
- try:
- cls.guard(flight)
- async with FlightRepository(flight) as flight_repo:
- await flight_repo.create_flight()
- except PyMongoError as e:
- logger.error(f"controllers.flight.create_flight: PyMongoError {e}")
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to create flight in db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.flight.create_flight: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to create flight: {exc_str}",
- ) from e
- else:
- return FlightCreated(flight_id=flight_repo.flight_id)
- finally:
- flight_id = (
- getattr(flight_repo, 'flight_id', 'unknown')
- if flight_repo
- else 'unknown'
- )
- logger.info(
- f"Call to controllers.flight.create_flight completed for Flight {flight_id}"
- )
-
- @staticmethod
- async def get_flight_by_id(
- flight_id: str,
- ) -> Union[FlightView, HTTPException]:
- """
- Get a flight from the database.
-
- Args:
- flight_id: str
-
- Returns:
- models.Flight
-
- Raises:
- HTTP 404 Not Found: If the flight is not found in the database.
- """
- try:
- async with FlightRepository() as flight_repo:
- await flight_repo.get_flight_by_id(flight_id)
- flight = flight_repo.flight
- except PyMongoError as e:
- logger.error(
- f"controllers.flight.get_flight_by_id: PyMongoError {e}"
- )
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to read flight from db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.flight.get_flight_by_id: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to read flight: {exc_str}",
- ) from e
- else:
- if flight:
- motor_view = MotorView(
- **flight.rocket.motor.dict(),
- selected_motor_kind=flight.rocket.motor.motor_kind,
- )
- updated_rocket = flight.rocket.dict()
- updated_rocket.update(motor=motor_view)
- rocket_view = RocketView(
- **updated_rocket,
- )
- updated_flight = flight.dict()
- updated_flight.update(rocket=rocket_view)
- flight_view = FlightView(**updated_flight)
- return flight_view
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail="Flight not found.",
- )
- finally:
- logger.info(
- f"Call to controllers.flight.get_flight_by_id completed for Flight {flight_id}"
- )
-
- @classmethod
- async def get_rocketpy_flight_binary(
- cls,
- flight_id: str,
- ) -> Union[bytes, HTTPException]:
- """
- Get rocketpy.flight as dill binary.
-
- Args:
- flight_id: str
-
- Returns:
- bytes
-
- Raises:
- HTTP 404 Not Found: If the flight is not found in the database.
- """
- try:
- flight = await cls.get_flight_by_id(flight_id)
- flight_service = FlightService.from_flight_model(flight)
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(
- f"controllers.flight.get_rocketpy_flight_binary: {exc_str}"
- )
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to read flight: {exc_str}",
- ) from e
- else:
- return flight_service.get_flight_binary()
- finally:
- logger.info(
- f"Call to controllers.flight.get_rocketpy_flight_binary completed for Flight {flight_id}"
- )
-
- @classmethod
- async def update_flight_by_id(
- cls, flight_id: str, flight: Flight
- ) -> Union[FlightUpdated, HTTPException]:
- """
- Update a models.Flight in the database.
-
- Args:
- flight_id: str
-
- Returns:
- views.FlightUpdated
-
- Raises:
- HTTP 404 Not Found: If the flight is not found in the database.
- """
- try:
- cls.guard(flight)
- async with FlightRepository(flight) as flight_repo:
- await flight_repo.update_flight_by_id(flight_id)
- except PyMongoError as e:
- logger.error(f"controllers.flight.update_flight: PyMongoError {e}")
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to update flight in db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.flight.update_flight: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to update flight: {exc_str}",
- ) from e
- else:
- return FlightUpdated(flight_id=flight_id)
- finally:
- logger.info(
- f"Call to controllers.flight.update_flight completed for Flight {flight_id}"
- )
-
- @classmethod
- async def update_env_by_flight_id(
- cls, flight_id: str, *, env: Env
- ) -> Union[FlightUpdated, HTTPException]:
- """
- Update a models.Flight.env in the database.
-
- Args:
- flight_id: str
- env: models.Env
-
- Returns:
- views.FlightUpdated
-
- Raises:
- HTTP 404 Not Found: If the flight is not found in the database.
- """
- try:
- flight = await cls.get_flight_by_id(flight_id)
- flight.environment = env
- async with FlightRepository(flight) as flight_repo:
- await flight_repo.update_env_by_flight_id(flight_id)
- except PyMongoError as e:
- logger.error(
- f"controllers.flight.update_env_by_flight_id: PyMongoError {e}"
- )
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to update environment from db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.flight.update_env: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to update environment: {exc_str}",
- ) from e
- else:
- return FlightUpdated(flight_id=flight_id)
- finally:
- logger.info(
- f"Call to controllers.flight.update_env completed for Flight {flight_id}"
- )
-
- @classmethod
- async def update_rocket_by_flight_id(
- cls, flight_id: str, *, rocket: Rocket
- ) -> Union[FlightUpdated, HTTPException]:
- """
- Update a models.Flight.rocket in the database.
-
- Args:
- flight_id: str
- rocket: models.Rocket
-
- Returns:
- views.FlightUpdated
-
- Raises:
- HTTP 404 Not Found: If the flight is not found in the database.
- """
- try:
- flight = await cls.get_flight_by_id(flight_id)
- flight.rocket = rocket
- async with FlightRepository(flight) as flight_repo:
- await flight_repo.update_rocket_by_flight_id(flight_id)
- except PyMongoError as e:
- logger.error(
- f"controllers.flight.update_rocket_by_flight_id: PyMongoError {e}"
- )
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to update rocket from db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.flight.update_rocket: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to update rocket: {exc_str}",
- ) from e
- else:
- return FlightUpdated(flight_id=flight_id)
- finally:
- logger.info(
- f"Call to controllers.flight.update_rocket completed for Flight {flight_id}"
- )
-
- @staticmethod
- async def delete_flight_by_id(
- flight_id: str,
- ) -> Union[FlightDeleted, HTTPException]:
- """
- Delete a models.Flight from the database.
-
- Args:
- flight_id: str
-
- Returns:
- views.FlightDeleted
-
- Raises:
- HTTP 404 Not Found: If the flight is not found in the database.
- """
- try:
- async with FlightRepository() as flight_repo:
- await flight_repo.delete_flight_by_id(flight_id)
- except PyMongoError as e:
- logger.error(f"controllers.flight.delete_flight: PyMongoError {e}")
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to delete flight from db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.flight.delete_flight: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to delete flight: {exc_str}",
- ) from e
- else:
- return FlightDeleted(flight_id=flight_id)
- finally:
- logger.info(
- f"Call to controllers.flight.delete_flight completed for Flight {flight_id}"
- )
-
- @classmethod
- async def simulate_flight(
- cls,
- flight_id: str,
- ) -> Union[FlightSummary, HTTPException]:
- """
- Simulate a rocket flight.
-
- Args:
- flight_id: str
-
- Returns:
- Flight summary view.
-
- Raises:
- HTTP 404 Not Found: If the flight does not exist in the database.
- """
- try:
- flight = await cls.get_flight_by_id(flight_id=flight_id)
- flight_service = FlightService.from_flight_model(flight)
- flight_summary = flight_service.get_flight_summary()
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.flight.simulate_flight: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to simulate flight, parameters may contain data that is not physically coherent: {exc_str}",
- ) from e
- else:
- return flight_summary
- finally:
- logger.info(
- f"Call to controllers.flight.simulate_flight completed for Flight {flight_id}"
- )
diff --git a/lib/controllers/motor.py b/lib/controllers/motor.py
deleted file mode 100644
index 81304fe..0000000
--- a/lib/controllers/motor.py
+++ /dev/null
@@ -1,289 +0,0 @@
-from typing import Union
-from fastapi import HTTPException, status
-from pymongo.errors import PyMongoError
-
-from lib import logger, parse_error
-from lib.models.motor import Motor, MotorKinds
-from lib.services.motor import MotorService
-from lib.repositories.motor import MotorRepository
-from lib.views.motor import (
- MotorSummary,
- MotorCreated,
- MotorUpdated,
- MotorDeleted,
- MotorView,
-)
-
-
-class MotorController:
- """
- Controller for the motor model.
-
- Enables:
- - Create a rocketpy.Motor object from a Motor model object.
- """
-
- @staticmethod
- def guard(motor: Motor):
- if (
- motor.motor_kind not in (MotorKinds.SOLID, MotorKinds.GENERIC)
- and motor.tanks is None
- ):
- raise HTTPException(
- status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
- detail="Tanks must be provided for liquid and hybrid motors.",
- )
-
- # TODO: extend guard to check motor kinds and tank kinds specifics
-
- @classmethod
- async def create_motor(
- cls, motor: Motor
- ) -> Union[MotorCreated, HTTPException]:
- """
- Create a models.Motor in the database.
-
- Returns:
- views.MotorCreated
- """
- motor_repo = None
- try:
- cls.guard(motor)
- async with MotorRepository(motor) as motor_repo:
- await motor_repo.create_motor()
- except PyMongoError as e:
- logger.error(f"controllers.motor.create_motor: PyMongoError {e}")
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to create motor in db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.motor.create_motor: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to create motor: {exc_str}",
- ) from e
- else:
- return MotorCreated(motor_id=motor_repo.motor_id)
- finally:
- motor_id = (
- getattr(motor_repo, 'motor_id', 'unknown')
- if motor_repo
- else 'unknown'
- )
- if motor_repo:
- logger.info(
- f"Call to controllers.motor.create_motor completed for Motor {motor_id}"
- )
-
- @staticmethod
- async def get_motor_by_id(
- motor_id: str,
- ) -> Union[MotorView, HTTPException]:
- """
- Get a models.Motor from the database.
-
- Args:
- motor_id: str
-
- Returns:
- models.Motor
-
- Raises:
- HTTP 404 Not Found: If the motor is not found in the database.
- """
- try:
- async with MotorRepository() as motor_repo:
- await motor_repo.get_motor_by_id(motor_id)
- motor = motor_repo.motor
- except PyMongoError as e:
- logger.error(
- f"controllers.motor.get_motor_by_id: PyMongoError {e}"
- )
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to read motor from db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.motor.get_motor_by_id: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to read motor: {exc_str}",
- ) from e
- else:
- if motor:
- motor_view = MotorView(
- **motor.dict(), selected_motor_kind=motor.motor_kind
- )
- return motor_view
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail="Motor not found",
- )
- finally:
- logger.info(
- f"Call to controllers.motor.get_motor_by_id completed for Motor {motor_id}"
- )
-
- @classmethod
- async def get_rocketpy_motor_binary(
- cls,
- motor_id: str,
- ) -> Union[bytes, HTTPException]:
- """
- Get a rocketpy.Motor object as a dill binary.
-
- Args:
- motor_id: str
-
- Returns:
- bytes
-
- Raises:
- HTTP 404 Not Found: If the motor is not found in the database.
- """
- try:
- motor = await cls.get_motor_by_id(motor_id)
- motor_service = MotorService.from_motor_model(motor)
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(
- f"controllers.motor.get_rocketpy_motor_binary: {exc_str}"
- )
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to read motor: {exc_str}",
- ) from e
- else:
- return motor_service.get_motor_binary()
- finally:
- logger.info(
- f"Call to controllers.motor.get_rocketpy_motor_binary completed for Motor {motor_id}"
- )
-
- @classmethod
- async def update_motor_by_id(
- cls, motor_id: str, motor: Motor
- ) -> Union[MotorUpdated, HTTPException]:
- """
- Update a motor in the database.
-
- Args:
- motor_id: str
-
- Returns:
- views.MotorUpdated
-
- Raises:
- HTTP 404 Not Found: If the motor is not found in the database.
- """
- try:
- cls.guard(motor)
- async with MotorRepository(motor) as motor_repo:
- await motor_repo.update_motor_by_id(motor_id)
- except PyMongoError as e:
- logger.error(f"controllers.motor.update_motor: PyMongoError {e}")
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to update motor in db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.motor.update_motor: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to update motor: {exc_str}",
- ) from e
- else:
- return MotorUpdated(motor_id=motor_id)
- finally:
- logger.info(
- f"Call to controllers.motor.update_motor completed for Motor {motor_id}"
- )
-
- @staticmethod
- async def delete_motor_by_id(
- motor_id: str,
- ) -> Union[MotorDeleted, HTTPException]:
- """
- Delete a models.Motor from the database.
-
- Args:
- motor_id: str
-
- Returns:
- views.MotorDeleted
-
- Raises:
- HTTP 404 Not Found: If the motor is not found in the database.
- """
- try:
- async with MotorRepository() as motor_repo:
- await motor_repo.delete_motor_by_id(motor_id)
- except PyMongoError as e:
- logger.error(f"controllers.motor.delete_motor: PyMongoError {e}")
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to delete motor from db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.motor.delete_motor: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to delete motor: {exc_str}",
- ) from e
- else:
- return MotorDeleted(motor_id=motor_id)
- finally:
- logger.info(
- f"Call to controllers.motor.delete_motor completed for Motor {motor_id}"
- )
-
- @classmethod
- async def simulate_motor(
- cls, motor_id: str
- ) -> Union[MotorSummary, HTTPException]:
- """
- Simulate a rocketpy motor.
-
- Args:
- motor_id: str
-
- Returns:
- views.MotorSummary
-
- Raises:
- HTTP 404 Not Found: If the motor does not exist in the database.
- """
- try:
- motor = await cls.get_motor_by_id(motor_id)
- motor_service = MotorService.from_motor_model(motor)
- motor_summary = motor_service.get_motor_summary()
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.motor.simulate_motor: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to simulate motor, parameters may contain data that is not physically coherent: {exc_str}",
- ) from e
- else:
- return motor_summary
- finally:
- logger.info(
- f"Call to controllers.motor.simulate_motor completed for Motor {motor_id}"
- )
diff --git a/lib/controllers/rocket.py b/lib/controllers/rocket.py
deleted file mode 100644
index 0bfea11..0000000
--- a/lib/controllers/rocket.py
+++ /dev/null
@@ -1,289 +0,0 @@
-from typing import Union
-
-from fastapi import HTTPException, status
-from pymongo.errors import PyMongoError
-
-from lib import logger, parse_error
-from lib.services.rocket import RocketService
-from lib.models.rocket import Rocket
-from lib.controllers.motor import MotorController
-from lib.repositories.rocket import RocketRepository
-from lib.views.motor import MotorView
-from lib.views.rocket import (
- RocketSummary,
- RocketCreated,
- RocketUpdated,
- RocketDeleted,
- RocketView,
-)
-
-
-class RocketController:
- """
- Controller for the Rocket model.
-
- Enables:
- - CRUD operations over models.Rocket on the database.
- """
-
- @staticmethod
- def guard(rocket: Rocket):
- MotorController.guard(rocket.motor)
-
- @classmethod
- async def create_rocket(
- cls, rocket: Rocket
- ) -> Union[RocketCreated, HTTPException]:
- """
- Create a models.Rocket in the database.
-
- Returns:
- views.RocketCreated
- """
- rocket_repo = None
- try:
- cls.guard(rocket)
- async with RocketRepository(rocket) as rocket_repo:
- await rocket_repo.create_rocket()
- except PyMongoError as e:
- logger.error(f"controllers.rocket.create_rocket: PyMongoError {e}")
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to create rocket in the db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.rocket.create_rocket: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to create rocket: {exc_str}",
- ) from e
- else:
- return RocketCreated(rocket_id=rocket_repo.rocket_id)
- finally:
- rocket_id = (
- getattr(rocket_repo, 'rocket_id', 'unknown')
- if rocket_repo
- else 'unknown'
- )
- if rocket_repo:
- logger.info(
- f"Call to controllers.rocket.create_rocket completed for Rocket {rocket_id}"
- )
-
- @staticmethod
- async def get_rocket_by_id(
- rocket_id: str,
- ) -> Union[RocketView, HTTPException]:
- """
- Get a rocket from the database.
-
- Args:
- rocket_id: str
-
- Returns:
- models.Rocket
-
- Raises:
- HTTP 404 Not Found: If the rocket is not found in the database.
- """
- try:
- async with RocketRepository() as rocket_repo:
- await rocket_repo.get_rocket_by_id(rocket_id)
- rocket = rocket_repo.rocket
- except PyMongoError as e:
- logger.error(
- f"controllers.rocket.get_rocket_by_id: PyMongoError {e}"
- )
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to read rocket from db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.rocket.get_rocket_by_id: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to read rocket: {exc_str}",
- ) from e
- else:
- if rocket:
- motor_view = MotorView(
- **rocket.motor.dict(),
- selected_motor_kind=rocket.motor.motor_kind,
- )
- updated_rocket = rocket.dict()
- updated_rocket.update(motor=motor_view)
- rocket_view = RocketView(
- **updated_rocket,
- )
- return rocket_view
- raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail="Rocket not found",
- )
- finally:
- logger.info(
- f"Call to controllers.rocket.get_rocket_by_id completed for Rocket {rocket_id}"
- )
-
- @classmethod
- async def get_rocketpy_rocket_binary(
- cls, rocket_id: str
- ) -> Union[bytes, HTTPException]:
- """
- Get a rocketpy.Rocket object as dill binary.
-
- Args:
- rocket_id: str
-
- Returns:
- bytes
-
- Raises:
- HTTP 404 Not Found: If the rocket is not found in the database.
- """
- try:
- rocket = await cls.get_rocket_by_id(rocket_id)
- rocket_service = RocketService.from_rocket_model(rocket)
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(
- f"controllers.rocket.get_rocketpy_rocket_binary: {exc_str}"
- )
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to read rocket: {exc_str}",
- ) from e
- else:
- return rocket_service.get_rocket_binary()
- finally:
- logger.info(
- f"Call to controllers.rocket.get_rocketpy_rocket_binary completed for Rocket {rocket_id}"
- )
-
- @classmethod
- async def update_rocket_by_id(
- cls, rocket_id: str, rocket: Rocket
- ) -> Union[RocketUpdated, HTTPException]:
- """
- Update a models.Rocket in the database.
-
- Args:
- rocket_id: str
-
- Returns:
- views.RocketUpdated
-
- Raises:
- HTTP 404 Not Found: If the rocket is not found in the database.
- """
- try:
- cls.guard(rocket)
- async with RocketRepository(rocket) as rocket_repo:
- await rocket_repo.update_rocket_by_id(rocket_id)
- except PyMongoError as e:
- logger.error(f"controllers.rocket.update_rocket: PyMongoError {e}")
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to update rocket in the db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.rocket.update_rocket: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to update rocket: {exc_str}",
- ) from e
- else:
- return RocketUpdated(rocket_id=rocket_id)
- finally:
- logger.info(
- f"Call to controllers.rocket.update_rocket completed for Rocket {rocket_id}"
- )
-
- @staticmethod
- async def delete_rocket_by_id(
- rocket_id: str,
- ) -> Union[RocketDeleted, HTTPException]:
- """
- Delete a models.Rocket from the database.
-
- Args:
- rocket_id: str
-
- Returns:
- views.RocketDeleted
-
- Raises:
- HTTP 404 Not Found: If the rocket is not found in the database.
- """
- try:
- async with RocketRepository() as rocket_repo:
- await rocket_repo.delete_rocket_by_id(rocket_id)
- except PyMongoError as e:
- logger.error(f"controllers.rocket.delete_rocket: PyMongoError {e}")
- raise HTTPException(
- status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
- detail="Failed to delete rocket from db",
- ) from e
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.rocket.delete_rocket: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to delete rocket: {exc_str}",
- ) from e
- else:
- return RocketDeleted(rocket_id=rocket_id)
- finally:
- logger.info(
- f"Call to controllers.rocket.delete_rocket completed for Rocket {rocket_id}"
- )
-
- @classmethod
- async def simulate_rocket(
- cls,
- rocket_id: str,
- ) -> Union[RocketSummary, HTTPException]:
- """
- Simulate a rocketpy rocket.
-
- Args:
- rocket_id: str
-
- Returns:
- views.RocketSummary
-
- Raises:
- HTTP 404 Not Found: If the rocket does not exist in the database.
- """
- try:
- rocket = await cls.get_rocket_by_id(rocket_id)
- rocket_service = RocketService.from_rocket_model(rocket)
- rocket_summary = rocket_service.get_rocket_summary()
- except HTTPException as e:
- raise e from e
- except Exception as e:
- exc_str = parse_error(e)
- logger.error(f"controllers.rocket.simulate: {exc_str}")
- raise HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail=f"Failed to simulate rocket, parameters may contain data that is not physically coherent: {exc_str}",
- ) from e
- else:
- return rocket_summary
- finally:
- logger.info(
- f"Call to controllers.rocket.simulate completed for Rocket {rocket_id}"
- )
diff --git a/lib/models/environment.py b/lib/models/environment.py
deleted file mode 100644
index 3b62940..0000000
--- a/lib/models/environment.py
+++ /dev/null
@@ -1,28 +0,0 @@
-import datetime
-from enum import Enum
-from typing import Optional
-from pydantic import BaseModel
-
-
-class AtmosphericModelTypes(str, Enum):
- STANDARD_ATMOSPHERE: str = "STANDARD_ATMOSPHERE"
- CUSTOM_ATMOSPHERE: str = "CUSTOM_ATMOSPHERE"
- WYOMING_SOUNDING: str = "WYOMING_SOUNDING"
- FORECAST: str = "FORECAST"
- REANALYSIS: str = "REANALYSIS"
- ENSEMBLE: str = "ENSEMBLE"
-
-
-class Env(BaseModel):
- latitude: float
- longitude: float
- elevation: Optional[int] = 1
-
- # Optional parameters
- atmospheric_model_type: AtmosphericModelTypes = (
- AtmosphericModelTypes.STANDARD_ATMOSPHERE
- )
- atmospheric_model_file: Optional[str] = None
- date: Optional[datetime.datetime] = (
- datetime.datetime.today() + datetime.timedelta(days=1)
- )
diff --git a/lib/models/flight.py b/lib/models/flight.py
deleted file mode 100644
index 7616475..0000000
--- a/lib/models/flight.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from enum import Enum
-from typing import Optional
-from pydantic import BaseModel
-from lib.models.rocket import Rocket
-from lib.models.environment import Env
-
-
-class EquationsOfMotion(str, Enum):
- STANDARD: str = "STANDARD"
- SOLID_PROPULSION: str = "SOLID_PROPULSION"
-
-
-class Flight(BaseModel):
- name: str = "Flight"
- environment: Env
- rocket: Rocket
- rail_length: float = 1
- time_overshoot: bool = True
- terminate_on_apogee: bool = True
- equations_of_motion: EquationsOfMotion = EquationsOfMotion.STANDARD
-
- # Optional parameters
- inclination: Optional[int] = None
- heading: Optional[int] = None
- # TODO: implement initial_solution
- max_time: Optional[int] = None
- max_time_step: Optional[float] = None
- min_time_step: Optional[int] = None
- rtol: Optional[float] = None
- atol: Optional[float] = None
- verbose: Optional[bool] = None
-
- def get_additional_parameters(self):
- return {
- key: value
- for key, value in self.dict().items()
- if value is not None
- and key
- not in [
- "name",
- "environment",
- "rocket",
- "rail_length",
- "time_overshoot",
- "terminate_on_apogee",
- "equations_of_motion",
- ]
- }
diff --git a/lib/models/motor.py b/lib/models/motor.py
deleted file mode 100644
index c8121bf..0000000
--- a/lib/models/motor.py
+++ /dev/null
@@ -1,119 +0,0 @@
-from enum import Enum
-from typing import Optional, Tuple, List, Union
-from pydantic import BaseModel, PrivateAttr
-
-
-class MotorKinds(str, Enum):
- HYBRID: str = "HYBRID"
- SOLID: str = "SOLID"
- GENERIC: str = "GENERIC"
- LIQUID: str = "LIQUID"
-
-
-class TankKinds(str, Enum):
- LEVEL: str = "LEVEL"
- MASS: str = "MASS"
- MASS_FLOW: str = "MASSFLOW"
- ULLAGE: str = "ULLAGE"
-
-
-class CoordinateSystemOrientation(str, Enum):
- NOZZLE_TO_COMBUSTION_CHAMBER: str = "NOZZLE_TO_COMBUSTION_CHAMBER"
- COMBUSTION_CHAMBER_TO_NOZZLE: str = "COMBUSTION_CHAMBER_TO_NOZZLE"
-
-
-class TankFluids(BaseModel):
- name: str
- density: float
-
-
-class InterpolationMethods(str, Enum):
- LINEAR: str = "LINEAR"
- SPLINE: str = "SPLINE"
- AKIMA: str = "AKIMA"
- POLYNOMIAL: str = "POLYNOMIAL"
- SHEPARD: str = "SHEPARD"
- RBF: str = "RBF"
-
-
-class MotorTank(BaseModel):
- # Required parameters
- geometry: List[Tuple[Tuple[float, float], float]]
- gas: TankFluids
- liquid: TankFluids
- flux_time: Tuple[float, float]
- position: float
- discretize: int
-
- # Level based tank parameters
- liquid_height: Optional[float] = None
-
- # Mass based tank parameters
- liquid_mass: Optional[float] = None
- gas_mass: Optional[float] = None
-
- # Mass flow based tank parameters
- gas_mass_flow_rate_in: Optional[float] = None
- gas_mass_flow_rate_out: Optional[float] = None
- liquid_mass_flow_rate_in: Optional[float] = None
- liquid_mass_flow_rate_out: Optional[float] = None
- initial_liquid_mass: Optional[float] = None
- initial_gas_mass: Optional[float] = None
-
- # Ullage based tank parameters
- ullage: Optional[float] = None
-
- # Optional parameters
- name: Optional[str] = None
-
- # Computed parameters
- tank_kind: TankKinds = TankKinds.MASS_FLOW
-
-
-class Motor(BaseModel):
- # Required parameters
- thrust_source: List[List[float]]
- burn_time: float
- nozzle_radius: float
- dry_mass: float
- dry_inertia: Tuple[float, float, float] = (0, 0, 0)
- center_of_dry_mass_position: float
-
- # Generic motor parameters
- chamber_radius: Optional[float] = None
- chamber_height: Optional[float] = None
- chamber_position: Optional[float] = None
- propellant_initial_mass: Optional[float] = None
- nozzle_position: Optional[float] = None
-
- # Liquid motor parameters
- tanks: Optional[List[MotorTank]] = None
-
- # Solid motor parameters
- grain_number: Optional[int] = None
- grain_density: Optional[float] = None
- grain_outer_radius: Optional[float] = None
- grain_initial_inner_radius: Optional[float] = None
- grain_initial_height: Optional[float] = None
- grains_center_of_mass_position: Optional[float] = None
- grain_separation: Optional[float] = None
-
- # Hybrid motor parameters
- throat_radius: Optional[float] = None
-
- # Optional parameters
- interpolation_method: InterpolationMethods = InterpolationMethods.LINEAR
- coordinate_system_orientation: CoordinateSystemOrientation = (
- CoordinateSystemOrientation.NOZZLE_TO_COMBUSTION_CHAMBER
- )
- reshape_thrust_curve: Union[bool, tuple] = False
-
- # Computed parameters
- _motor_kind: MotorKinds = PrivateAttr(default=MotorKinds.SOLID)
-
- @property
- def motor_kind(self) -> MotorKinds:
- return self._motor_kind
-
- def set_motor_kind(self, motor_kind: MotorKinds):
- self._motor_kind = motor_kind
diff --git a/lib/models/rocket.py b/lib/models/rocket.py
deleted file mode 100644
index ab45419..0000000
--- a/lib/models/rocket.py
+++ /dev/null
@@ -1,42 +0,0 @@
-from enum import Enum
-from typing import Optional, Tuple, List, Union
-from pydantic import BaseModel
-from lib.models.motor import Motor
-from lib.models.aerosurfaces import (
- Fins,
- NoseCone,
- Tail,
- RailButtons,
- Parachute,
-)
-
-
-class CoordinateSystemOrientation(str, Enum):
- TAIL_TO_NOSE: str = "TAIL_TO_NOSE"
- NOSE_TO_TAIL: str = "NOSE_TO_TAIL"
-
-
-class Rocket(BaseModel):
-
- # Required parameters
- motor: Motor
- radius: float
- mass: float
- motor_position: float
- center_of_mass_without_motor: int
- inertia: Union[
- Tuple[float, float, float],
- Tuple[float, float, float, float, float, float],
- ] = (0, 0, 0)
- power_off_drag: List[Tuple[float, float]] = [(0, 0)]
- power_on_drag: List[Tuple[float, float]] = [(0, 0)]
- coordinate_system_orientation: CoordinateSystemOrientation = (
- CoordinateSystemOrientation.TAIL_TO_NOSE
- )
- nose: NoseCone
- fins: List[Fins]
-
- # Optional parameters
- parachutes: Optional[List[Parachute]] = None
- rail_buttons: Optional[RailButtons] = None
- tail: Optional[Tail] = None
diff --git a/lib/repositories/__init__.py b/lib/repositories/__init__.py
deleted file mode 100644
index c8add6a..0000000
--- a/lib/repositories/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# lib/controllers/__init__.py
diff --git a/lib/repositories/environment.py b/lib/repositories/environment.py
deleted file mode 100644
index 56dac18..0000000
--- a/lib/repositories/environment.py
+++ /dev/null
@@ -1,142 +0,0 @@
-from typing import Self
-from bson import ObjectId
-from pymongo.errors import PyMongoError
-from lib.models.environment import Env
-from lib import logger
-from lib.repositories.repo import Repository, RepositoryNotInitializedException
-
-
-class EnvRepository(Repository):
- """
- Enables database CRUD operations with models.Env
-
- Init Attributes:
- environment: models.Env
- """
-
- def __init__(self, environment: Env = None):
- super().__init__("environments")
- self._env = environment
- self._env_id = None
-
- @property
- def env(self) -> Env:
- return self._env
-
- @env.setter
- def env(self, environment: "Env"):
- self._env = environment
-
- @property
- def env_id(self) -> str:
- return str(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):
- collection = self.get_collection()
- result = await collection.insert_one(env_data)
- self.env_id = result.inserted_id
- return self
-
- async def update_env(self, env_data: dict, env_id: str):
- collection = self.get_collection()
- await collection.update_one(
- {"_id": ObjectId(env_id)}, {"$set": env_data}
- )
- return self
-
- async def find_env(self, env_id: str):
- collection = self.get_collection()
- return await collection.find_one({"_id": ObjectId(env_id)})
-
- async def delete_env(self, env_id: str):
- collection = self.get_collection()
- await collection.delete_one({"_id": ObjectId(env_id)})
- return self
-
- async def create_env(self):
- """
- Creates a models.Env in the database
-
- Returns:
- self
- """
- try:
- environment_to_dict = self.env.dict()
- await self.insert_env(environment_to_dict)
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.environment.create_env completed for Env {self.env_id}"
- )
-
- async def get_env_by_id(self, env_id: str) -> Self:
- """
- Gets a models.Env from the database
-
- Returns:
- self
- """
- try:
- read_env = await self.find_env(env_id)
- parsed_env = Env.parse_obj(read_env) if read_env else None
- self.env = parsed_env
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.environment.get_env completed for Env {env_id}"
- )
-
- async def delete_env_by_id(self, env_id: str):
- """
- Deletes a models.Env from the database
-
- Returns:
- self
- """
- try:
- await self.delete_env(env_id)
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.environment.delete_env completed for Env {env_id}"
- )
-
- async def update_env_by_id(self, env_id: str):
- """
- Updates a models.Env in the database
-
- Returns:
- self
- """
- try:
- environment_to_dict = self.env.dict()
- await self.update_env(environment_to_dict, env_id)
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.environment.update_env_by_id completed for Env {env_id}"
- )
diff --git a/lib/repositories/flight.py b/lib/repositories/flight.py
deleted file mode 100644
index 0c5f1a9..0000000
--- a/lib/repositories/flight.py
+++ /dev/null
@@ -1,212 +0,0 @@
-from typing import Self
-from bson import ObjectId
-from pymongo.errors import PyMongoError
-from lib import logger
-from lib.models.flight import Flight
-from lib.models.motor import MotorKinds
-from lib.repositories.repo import Repository, RepositoryNotInitializedException
-
-
-class FlightRepository(Repository):
- """
- Enables database CRUD operations with models.Flight
-
- Init Attributes:
- flight: models.Flight
- """
-
- def __init__(self, flight: Flight = None):
- super().__init__("flights")
- self._flight = flight
- self._flight_id = None
-
- @property
- def flight(self) -> Flight:
- return self._flight
-
- @flight.setter
- def flight(self, flight: "Flight"):
- self._flight = flight
-
- @property
- def flight_id(self) -> str:
- return str(self._flight_id)
-
- @flight_id.setter
- def flight_id(self, flight_id: "str"):
- self._flight_id = flight_id
-
- async def insert_flight(self, flight_data: dict):
- collection = self.get_collection()
- result = await collection.insert_one(flight_data)
- self.flight_id = result.inserted_id
- return self
-
- async def update_flight(self, flight_data: dict, flight_id: str):
- collection = self.get_collection()
- await collection.update_one(
- {"_id": ObjectId(flight_id)}, {"$set": flight_data}
- )
- return self
-
- async def update_env(self, env_data: dict, flight_id: str):
- collection = self.get_collection()
- await collection.update_one(
- {"_id": ObjectId(flight_id)}, {"$set": {"environment": env_data}}
- )
- return self
-
- async def update_rocket(self, rocket_data: dict, flight_id: str):
- collection = self.get_collection()
- await collection.update_one(
- {"_id": ObjectId(flight_id)}, {"$set": {"rocket": rocket_data}}
- )
- return self
-
- async def find_flight(self, flight_id: str):
- collection = self.get_collection()
- return await collection.find_one({"_id": ObjectId(flight_id)})
-
- async def delete_flight(self, flight_id: str):
- collection = self.get_collection()
- await collection.delete_one({"_id": ObjectId(flight_id)})
- return self
-
- async def create_flight(self):
- """
- Creates a models.Flight in the database
-
- Returns:
- self
- """
- try:
- flight_to_dict = self.flight.dict()
- flight_to_dict["rocket"]["motor"][
- "motor_kind"
- ] = self.flight.rocket.motor.motor_kind.value
- await self.insert_flight(flight_to_dict)
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.flight.create_flight completed for Flight {self.flight_id}"
- )
-
- async def get_flight_by_id(self, flight_id: str) -> Self:
- """
- Gets a models.Flight from the database
-
- Returns:
- self
- """
- try:
- read_flight = await self.find_flight(flight_id)
- if read_flight:
- parsed_flight = Flight.parse_obj(read_flight)
- parsed_flight.rocket.motor.set_motor_kind(
- MotorKinds(read_flight["rocket"]["motor"]["motor_kind"])
- )
- self.flight = parsed_flight
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.flight.get_flight completed for Flight {flight_id}"
- )
-
- async def delete_flight_by_id(self, flight_id: str):
- """
- Deletes a models.Flight from the database
-
- Returns:
- self
- """
- try:
- await self.delete_flight(flight_id)
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.flight.delete_flight completed for Flight {flight_id}"
- )
-
- async def update_flight_by_id(self, flight_id: str):
- """
- Updates a models.Flight in the database
-
- Returns:
- self
- """
- try:
- flight_to_dict = self.flight.dict()
- flight_to_dict["rocket"]["motor"][
- "motor_kind"
- ] = self.flight.rocket.motor.motor_kind.value
- await self.update_flight(flight_to_dict, flight_id)
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.flight.update_flight_by_id completed for Flight {flight_id}"
- )
-
- async def update_env_by_flight_id(self, flight_id: str):
- """
- Updates a models.Flight.Env in the database
-
- Returns:
- self
- """
- try:
- env_to_dict = self.flight.environment.dict()
- await self.update_env(env_to_dict, flight_id)
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.flight.update_env_by_flight_id completed for Flight {flight_id}"
- )
-
- async def update_rocket_by_flight_id(self, flight_id: str):
- """
- Updates a models.Flight.Rocket in the database
-
- Returns:
- self
- """
- try:
- rocket_to_dict = self.flight.rocket.dict()
- rocket_to_dict["motor"][
- "motor_kind"
- ] = self.flight.rocket.motor.motor_kind.value
- await self.update_rocket(rocket_to_dict, flight_id)
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.flight.update_rocket_by_flight_id completed for Flight {flight_id}"
- )
diff --git a/lib/repositories/motor.py b/lib/repositories/motor.py
deleted file mode 100644
index f63be78..0000000
--- a/lib/repositories/motor.py
+++ /dev/null
@@ -1,148 +0,0 @@
-from typing import Self
-from bson import ObjectId
-from pymongo.errors import PyMongoError
-from lib import logger
-from lib.models.motor import Motor, MotorKinds
-from lib.repositories.repo import Repository, RepositoryNotInitializedException
-
-
-class MotorRepository(Repository):
- """
- Enables database CRUD operations with models.Motor
-
- Init Attributes:
- motor: models.Motor
- """
-
- def __init__(self, motor: Motor = None):
- super().__init__("motors")
- self._motor = motor
- self._motor_id = None
-
- @property
- def motor(self) -> Motor:
- return self._motor
-
- @motor.setter
- def motor(self, motor: "Motor"):
- self._motor = motor
-
- @property
- def motor_id(self) -> str:
- return str(self._motor_id)
-
- @motor_id.setter
- def motor_id(self, motor_id: "str"):
- self._motor_id = motor_id
-
- async def insert_motor(self, motor_data: dict):
- collection = self.get_collection()
- result = await collection.insert_one(motor_data)
- self.motor_id = result.inserted_id
- return self
-
- async def update_motor(self, motor_data: dict, motor_id: str):
- collection = self.get_collection()
- await collection.update_one(
- {"_id": ObjectId(motor_id)}, {"$set": motor_data}
- )
- return self
-
- async def find_motor(self, motor_id: str):
- collection = self.get_collection()
- return await collection.find_one({"_id": ObjectId(motor_id)})
-
- async def delete_motor(self, motor_id: str):
- collection = self.get_collection()
- await collection.delete_one({"_id": ObjectId(motor_id)})
- return self
-
- async def create_motor(self):
- """
- Creates a models.Motor in the database
-
- Returns:
- self
- """
- try:
- motor_to_dict = self.motor.dict()
- motor_to_dict["motor_kind"] = self.motor.motor_kind.value
- await self.insert_motor(motor_to_dict)
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.motor.create_motor completed for Motor {self.motor_id}"
- )
-
- async def get_motor_by_id(self, motor_id: str) -> Self:
- """
- Gets a models.Motor from the database
-
- Returns:
- self
- """
- try:
- read_motor = await self.find_motor(motor_id)
- if read_motor:
- parsed_motor = Motor.parse_obj(read_motor)
- parsed_motor.set_motor_kind(
- MotorKinds(read_motor["motor_kind"])
- )
- self.motor = parsed_motor
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.motor.get_motor completed for Motor {motor_id}"
- )
-
- async def delete_motor_by_id(self, motor_id: str):
- """
- Deletes a models.Motor from the database
-
- Returns:
- self
- """
- try:
- await self.delete_motor(motor_id)
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.motor.delete_motor completed for Motor {motor_id}"
- )
-
- async def update_motor_by_id(self, motor_id: str):
- """
- Updates a models.Motor in the database
-
- Returns:
- self
- """
- try:
- motor_to_dict = self.motor.dict()
- motor_to_dict["motor_kind"] = self.motor.motor_kind.value
- await self.update_motor(motor_to_dict, motor_id)
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.motor.update_motor completed for Motor {motor_id}"
- )
diff --git a/lib/repositories/rocket.py b/lib/repositories/rocket.py
deleted file mode 100644
index 951f9f1..0000000
--- a/lib/repositories/rocket.py
+++ /dev/null
@@ -1,153 +0,0 @@
-from typing import Self
-from bson import ObjectId
-from pymongo.errors import PyMongoError
-from lib import logger
-from lib.models.rocket import Rocket
-from lib.models.motor import MotorKinds
-from lib.repositories.repo import Repository, RepositoryNotInitializedException
-
-
-class RocketRepository(Repository):
- """
- Enables database CRUD operations with models.Rocket
-
- Init Attributes:
- rocket: models.Rocket
- """
-
- def __init__(self, rocket: Rocket = None):
- super().__init__("rockets")
- self._rocket_id = None
- self._rocket = rocket
-
- @property
- def rocket(self) -> Rocket:
- return self._rocket
-
- @rocket.setter
- def rocket(self, rocket: "Rocket"):
- self._rocket = rocket
-
- @property
- def rocket_id(self) -> str:
- return str(self._rocket_id)
-
- @rocket_id.setter
- def rocket_id(self, rocket_id: "str"):
- self._rocket_id = rocket_id
-
- async def insert_rocket(self, rocket_data: dict):
- collection = self.get_collection()
- result = await collection.insert_one(rocket_data)
- self.rocket_id = result.inserted_id
- return self
-
- async def update_rocket(self, rocket_data: dict, rocket_id: str):
- collection = self.get_collection()
- await collection.update_one(
- {"_id": ObjectId(rocket_id)}, {"$set": rocket_data}
- )
- return self
-
- async def find_rocket(self, rocket_id: str):
- collection = self.get_collection()
- return await collection.find_one({"_id": ObjectId(rocket_id)})
-
- async def delete_rocket(self, rocket_id: str):
- collection = self.get_collection()
- await collection.delete_one({"_id": ObjectId(rocket_id)})
- return self
-
- async def create_rocket(self):
- """
- Creates a models.Rocket in the database
-
- Returns:
- self
- """
- try:
- rocket_to_dict = self.rocket.dict()
- rocket_to_dict["motor"][
- "motor_kind"
- ] = self.rocket.motor.motor_kind.value
- await self.insert_rocket(rocket_to_dict)
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.rocket.create_rocket completed for Rocket {self.rocket_id}"
- )
-
- async def get_rocket_by_id(self, rocket_id: str) -> Self:
- """
- Gets a models.Rocket from the database
-
- Returns:
- self
- """
- try:
- read_rocket = await self.find_rocket(rocket_id)
- if read_rocket:
- parsed_rocket = Rocket.parse_obj(read_rocket)
- parsed_rocket.motor.set_motor_kind(
- MotorKinds(read_rocket["motor"]["motor_kind"])
- )
- self.rocket = parsed_rocket
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.rocket.get_rocket completed for Rocket {rocket_id}"
- )
-
- async def delete_rocket_by_id(self, rocket_id: str):
- """
- Deletes a models.Rocket from the database
-
- Returns:
- self
- """
- try:
- await self.delete_rocket(rocket_id)
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.rocket.delete_rocket completed for Rocket {rocket_id}"
- )
-
- async def update_rocket_by_id(self, rocket_id: str):
- """
- Updates a models.Rocket in the database
-
- Returns:
- self
- """
- try:
- rocket_to_dict = self.rocket.dict()
- rocket_to_dict["motor"][
- "motor_kind"
- ] = self.rocket.motor.motor_kind.value
- await self.update_rocket(rocket_to_dict, rocket_id)
- except PyMongoError as e:
- raise e from e
- except RepositoryNotInitializedException as e:
- raise e from e
- else:
- return self
- finally:
- logger.info(
- f"Call to repositories.rocket.update_rocket_by_id completed for Rocket {rocket_id}"
- )
diff --git a/lib/routes/environment.py b/lib/routes/environment.py
deleted file mode 100644
index 6834ed3..0000000
--- a/lib/routes/environment.py
+++ /dev/null
@@ -1,121 +0,0 @@
-"""
-Environment routes
-"""
-
-from fastapi import APIRouter, Response
-from opentelemetry import trace
-
-from lib.views.environment import (
- EnvSummary,
- EnvCreated,
- EnvUpdated,
- EnvDeleted,
-)
-from lib.models.environment import Env
-from lib.controllers.environment import EnvController
-
-router = APIRouter(
- prefix="/environments",
- tags=["ENVIRONMENT"],
- responses={
- 404: {"description": "Not found"},
- 422: {"description": "Unprocessable Entity"},
- 500: {"description": "Internal Server Error"},
- },
-)
-
-tracer = trace.get_tracer(__name__)
-
-
-@router.post("/")
-async def create_env(env: Env) -> EnvCreated:
- """
- Creates a new environment
-
- ## Args
- ``` models.Env JSON ```
- """
- with tracer.start_as_current_span("create_env"):
- return await EnvController.create_env(env)
-
-
-@router.get("/{env_id}")
-async def read_env(env_id: str) -> Env:
- """
- Reads an environment
-
- ## Args
- ``` env_id: str ```
- """
- with tracer.start_as_current_span("read_env"):
- return await EnvController.get_env_by_id(env_id)
-
-
-@router.put("/{env_id}")
-async def update_env(env_id: str, env: Env) -> EnvUpdated:
- """
- Updates an environment
-
- ## Args
- ```
- env_id: str
- env: models.Env JSON
- ```
- """
- with tracer.start_as_current_span("update_env"):
- return await EnvController.update_env_by_id(env_id, env)
-
-
-@router.get(
- "/{env_id}/rocketpy",
- responses={
- 203: {
- "description": "Binary file download",
- "content": {"application/octet-stream": {}},
- }
- },
- status_code=203,
- response_class=Response,
-)
-async def read_rocketpy_env(env_id: str):
- """
- Loads rocketpy.environment as a dill binary
-
- ## Args
- ``` env_id: str ```
- """
- with tracer.start_as_current_span("read_rocketpy_env"):
- headers = {
- 'Content-Disposition': f'attachment; filename="rocketpy_environment_{env_id}.dill"'
- }
- binary = await EnvController.get_rocketpy_env_binary(env_id)
- return Response(
- content=binary,
- headers=headers,
- media_type="application/octet-stream",
- status_code=203,
- )
-
-
-@router.get("/{env_id}/summary")
-async def simulate_env(env_id: str) -> EnvSummary:
- """
- Loads rocketpy.environment simulation
-
- ## Args
- ``` env_id: str ```
- """
- with tracer.start_as_current_span("simulate_env"):
- return await EnvController.simulate_env(env_id)
-
-
-@router.delete("/{env_id}")
-async def delete_env(env_id: str) -> EnvDeleted:
- """
- Deletes an environment
-
- ## Args
- ``` env_id: str ```
- """
- with tracer.start_as_current_span("delete_env"):
- return await EnvController.delete_env_by_id(env_id)
diff --git a/lib/routes/flight.py b/lib/routes/flight.py
deleted file mode 100644
index e88b87a..0000000
--- a/lib/routes/flight.py
+++ /dev/null
@@ -1,173 +0,0 @@
-"""
-Flight routes
-"""
-
-from fastapi import APIRouter, Response
-from opentelemetry import trace
-
-from lib.views.flight import (
- FlightSummary,
- FlightCreated,
- FlightUpdated,
- FlightDeleted,
-)
-from lib.models.environment import Env
-from lib.models.flight import Flight
-from lib.models.rocket import Rocket
-from lib.models.motor import MotorKinds
-from lib.views.flight import FlightView
-from lib.controllers.flight import FlightController
-
-router = APIRouter(
- prefix="/flights",
- tags=["FLIGHT"],
- responses={
- 404: {"description": "Not found"},
- 422: {"description": "Unprocessable Entity"},
- 500: {"description": "Internal Server Error"},
- },
-)
-
-tracer = trace.get_tracer(__name__)
-
-
-@router.post("/")
-async def create_flight(
- flight: Flight, motor_kind: MotorKinds
-) -> FlightCreated:
- """
- Creates a new flight
-
- ## Args
- ``` Flight object as JSON ```
- """
- with tracer.start_as_current_span("create_flight"):
- flight.rocket.motor.set_motor_kind(motor_kind)
- return await FlightController.create_flight(flight)
-
-
-@router.get("/{flight_id}")
-async def read_flight(flight_id: str) -> FlightView:
- """
- Reads a flight
-
- ## Args
- ``` flight_id: Flight ID ```
- """
- with tracer.start_as_current_span("read_flight"):
- return await FlightController.get_flight_by_id(flight_id)
-
-
-@router.get(
- "/{flight_id}/rocketpy",
- responses={
- 203: {
- "description": "Binary file download",
- "content": {"application/octet-stream": {}},
- }
- },
- status_code=203,
- response_class=Response,
-)
-async def read_rocketpy_flight(flight_id: str):
- """
- Loads rocketpy.flight as a dill binary
-
- ## Args
- ``` flight_id: str ```
- """
- with tracer.start_as_current_span("read_rocketpy_flight"):
- headers = {
- 'Content-Disposition': f'attachment; filename="rocketpy_flight_{flight_id}.dill"'
- }
- binary = await FlightController.get_rocketpy_flight_binary(flight_id)
- return Response(
- content=binary,
- headers=headers,
- media_type="application/octet-stream",
- status_code=203,
- )
-
-
-@router.put("/{flight_id}/env")
-async def update_flight_env(flight_id: str, env: Env) -> FlightUpdated:
- """
- Updates flight environment
-
- ## Args
- ```
- flight_id: Flight ID
- env: env object as JSON
- ```
- """
- with tracer.start_as_current_span("update_flight_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: str,
- rocket: Rocket,
- motor_kind: MotorKinds,
-) -> FlightUpdated:
- """
- Updates flight rocket.
-
- ## Args
- ```
- flight_id: Flight ID
- rocket: Rocket object as JSON
- ```
- """
- with tracer.start_as_current_span("update_flight_rocket"):
- rocket.motor.set_motor_kind(motor_kind)
- return await FlightController.update_rocket_by_flight_id(
- flight_id,
- rocket=rocket,
- )
-
-
-@router.put("/{flight_id}")
-async def update_flight(
- flight_id: str,
- flight: Flight,
- motor_kind: MotorKinds,
-) -> FlightUpdated:
- """
- Updates Flight object
-
- ## Args
- ```
- flight_id: Flight ID
- flight: Flight object as JSON
- ```
- """
- with tracer.start_as_current_span("update_flight"):
- flight.rocket.motor.set_motor_kind(motor_kind)
- return await FlightController.update_flight_by_id(flight_id, flight)
-
-
-@router.get("/{flight_id}/summary")
-async def simulate_flight(flight_id: str) -> FlightSummary:
- """
- Simulates a flight
-
- ## Args
- ``` flight_id: Flight ID ```
- """
- with tracer.start_as_current_span("simulate_flight"):
- return await FlightController.simulate_flight(flight_id)
-
-
-@router.delete("/{flight_id}")
-async def delete_flight(flight_id: str) -> FlightDeleted:
- """
- Deletes a flight
-
- ## Args
- ``` flight_id: Flight ID ```
- """
- with tracer.start_as_current_span("delete_flight"):
- return await FlightController.delete_flight_by_id(flight_id)
diff --git a/lib/routes/motor.py b/lib/routes/motor.py
deleted file mode 100644
index 5a89da7..0000000
--- a/lib/routes/motor.py
+++ /dev/null
@@ -1,126 +0,0 @@
-"""
-Motor routes
-"""
-
-from fastapi import APIRouter, Response
-from opentelemetry import trace
-
-from lib.views.motor import (
- MotorSummary,
- MotorCreated,
- MotorUpdated,
- MotorDeleted,
-)
-from lib.models.motor import Motor, MotorKinds
-from lib.controllers.motor import MotorController
-from lib.views.motor import MotorView
-
-router = APIRouter(
- prefix="/motors",
- tags=["MOTOR"],
- responses={
- 404: {"description": "Not found"},
- 422: {"description": "Unprocessable Entity"},
- 500: {"description": "Internal Server Error"},
- },
-)
-
-tracer = trace.get_tracer(__name__)
-
-
-@router.post("/")
-async def create_motor(motor: Motor, motor_kind: MotorKinds) -> MotorCreated:
- """
- Creates a new motor
-
- ## Args
- ``` Motor object as a JSON ```
- """
- with tracer.start_as_current_span("create_motor"):
- motor.set_motor_kind(motor_kind)
- return await MotorController.create_motor(motor)
-
-
-@router.get("/{motor_id}")
-async def read_motor(motor_id: str) -> MotorView:
- """
- Reads a motor
-
- ## Args
- ``` motor_id: Motor ID ```
- """
- with tracer.start_as_current_span("read_motor"):
- return await MotorController.get_motor_by_id(motor_id)
-
-
-@router.put("/{motor_id}")
-async def update_motor(
- motor_id: str, motor: Motor, motor_kind: MotorKinds
-) -> MotorUpdated:
- """
- Updates a motor
-
- ## Args
- ```
- motor_id: Motor ID
- motor: Motor object as JSON
- ```
- """
- with tracer.start_as_current_span("update_motor"):
- motor.set_motor_kind(motor_kind)
- return await MotorController.update_motor_by_id(motor_id, motor)
-
-
-@router.get(
- "/{motor_id}/rocketpy",
- responses={
- 203: {
- "description": "Binary file download",
- "content": {"application/octet-stream": {}},
- }
- },
- status_code=203,
- response_class=Response,
-)
-async def read_rocketpy_motor(motor_id: str):
- """
- Loads rocketpy.motor as a dill binary
-
- ## Args
- ``` motor_id: str ```
- """
- with tracer.start_as_current_span("read_rocketpy_motor"):
- headers = {
- 'Content-Disposition': f'attachment; filename="rocketpy_motor_{motor_id}.dill"'
- }
- binary = await MotorController.get_rocketpy_motor_binary(motor_id)
- return Response(
- content=binary,
- headers=headers,
- media_type="application/octet-stream",
- status_code=203,
- )
-
-
-@router.get("/{motor_id}/summary")
-async def simulate_motor(motor_id: str) -> MotorSummary:
- """
- Simulates a motor
-
- ## Args
- ``` motor_id: Motor ID ```
- """
- with tracer.start_as_current_span("simulate_motor"):
- return await MotorController.simulate_motor(motor_id)
-
-
-@router.delete("/{motor_id}")
-async def delete_motor(motor_id: str) -> MotorDeleted:
- """
- Deletes a motor
-
- ## Args
- ``` motor_id: Motor ID ```
- """
- with tracer.start_as_current_span("delete_motor"):
- return await MotorController.delete_motor_by_id(motor_id)
diff --git a/lib/routes/rocket.py b/lib/routes/rocket.py
deleted file mode 100644
index f847754..0000000
--- a/lib/routes/rocket.py
+++ /dev/null
@@ -1,131 +0,0 @@
-"""
-Rocket routes
-"""
-
-from fastapi import APIRouter, Response
-from opentelemetry import trace
-
-from lib.views.rocket import (
- RocketSummary,
- RocketCreated,
- RocketUpdated,
- RocketDeleted,
-)
-from lib.models.rocket import Rocket
-from lib.models.motor import MotorKinds
-from lib.views.rocket import RocketView
-from lib.controllers.rocket import RocketController
-
-router = APIRouter(
- prefix="/rockets",
- tags=["ROCKET"],
- responses={
- 404: {"description": "Not found"},
- 422: {"description": "Unprocessable Entity"},
- 500: {"description": "Internal Server Error"},
- },
-)
-
-tracer = trace.get_tracer(__name__)
-
-
-@router.post("/")
-async def create_rocket(
- rocket: Rocket, motor_kind: MotorKinds
-) -> RocketCreated:
- """
- Creates a new rocket
-
- ## Args
- ``` Rocket object as a JSON ```
- """
- with tracer.start_as_current_span("create_rocket"):
- rocket.motor.set_motor_kind(motor_kind)
- return await RocketController.create_rocket(rocket)
-
-
-@router.get("/{rocket_id}")
-async def read_rocket(rocket_id: str) -> RocketView:
- """
- Reads a rocket
-
- ## Args
- ``` rocket_id: Rocket ID ```
- """
- with tracer.start_as_current_span("read_rocket"):
- return await RocketController.get_rocket_by_id(rocket_id)
-
-
-@router.put("/{rocket_id}")
-async def update_rocket(
- rocket_id: str,
- rocket: Rocket,
- motor_kind: MotorKinds,
-) -> RocketUpdated:
- """
- Updates a rocket
-
- ## Args
- ```
- rocket_id: Rocket ID
- rocket: Rocket object as JSON
- ```
- """
- with tracer.start_as_current_span("update_rocket"):
- rocket.motor.set_motor_kind(motor_kind)
- return await RocketController.update_rocket_by_id(rocket_id, rocket)
-
-
-@router.get(
- "/{rocket_id}/rocketpy",
- responses={
- 203: {
- "description": "Binary file download",
- "content": {"application/octet-stream": {}},
- }
- },
- status_code=203,
- response_class=Response,
-)
-async def read_rocketpy_rocket(rocket_id: str):
- """
- Loads rocketpy.rocket as a dill binary
-
- ## Args
- ``` rocket_id: str ```
- """
- with tracer.start_as_current_span("read_rocketpy_rocket"):
- headers = {
- 'Content-Disposition': f'attachment; filename="rocketpy_rocket_{rocket_id}.dill"'
- }
- binary = await RocketController.get_rocketpy_rocket_binary(rocket_id)
- return Response(
- content=binary,
- headers=headers,
- media_type="application/octet-stream",
- status_code=203,
- )
-
-
-@router.get("/{rocket_id}/summary")
-async def simulate_rocket(rocket_id: str) -> RocketSummary:
- """
- Simulates a rocket
-
- ## Args
- ``` rocket_id: Rocket ID ```
- """
- with tracer.start_as_current_span("simulate_rocket"):
- return await RocketController.simulate_rocket(rocket_id)
-
-
-@router.delete("/{rocket_id}")
-async def delete_rocket(rocket_id: str) -> RocketDeleted:
- """
- Deletes a rocket
-
- ## Args
- ``` rocket_id: Rocket ID ```
- """
- with tracer.start_as_current_span("delete_rocket"):
- return await RocketController.delete_rocket_by_id(rocket_id)
diff --git a/lib/views/environment.py b/lib/views/environment.py
deleted file mode 100644
index e3fcd31..0000000
--- a/lib/views/environment.py
+++ /dev/null
@@ -1,67 +0,0 @@
-from typing import Optional, Any
-from datetime import datetime, timedelta
-from pydantic import BaseModel, ConfigDict
-from lib.models.environment import AtmosphericModelTypes
-from lib.utils import to_python_primitive
-
-
-class EnvSummary(BaseModel):
- latitude: Optional[float] = None
- longitude: Optional[float] = None
- elevation: Optional[float] = 1
- atmospheric_model_type: Optional[str] = (
- AtmosphericModelTypes.STANDARD_ATMOSPHERE.value
- )
- air_gas_constant: Optional[float] = None
- standard_g: Optional[float] = None
- earth_radius: Optional[float] = None
- datum: Optional[str] = None
- timezone: Optional[str] = None
- initial_utm_zone: Optional[int] = None
- initial_utm_letter: Optional[str] = None
- initial_north: Optional[float] = None
- initial_east: Optional[float] = None
- initial_hemisphere: Optional[str] = None
- initial_ew: Optional[str] = None
- max_expected_height: Optional[int] = None
- date: Optional[datetime] = datetime.today() + timedelta(days=1)
- local_date: Optional[datetime] = datetime.today() + timedelta(days=1)
- datetime_date: Optional[datetime] = datetime.today() + timedelta(days=1)
- ellipsoid: Optional[Any] = None
- barometric_height: Optional[Any] = None
- barometric_height_ISA: Optional[Any] = None
- pressure: Optional[Any] = None
- pressure_ISA: Optional[Any] = None
- temperature: Optional[Any] = None
- temperature_ISA: Optional[Any] = None
- density: Optional[Any] = None
- speed_of_sound: Optional[Any] = None
- dynamic_viscosity: Optional[Any] = None
- gravity: Optional[Any] = None
- somigliana_gravity: Optional[Any] = None
- wind_speed: Optional[Any] = None
- wind_direction: Optional[Any] = None
- wind_heading: Optional[Any] = None
- wind_velocity_x: Optional[Any] = None
- wind_velocity_y: Optional[Any] = None
- calculate_earth_radius: Optional[Any] = None
- decimal_degrees_to_arc_seconds: Optional[Any] = None
- geodesic_to_utm: Optional[Any] = None
- utm_to_geodesic: Optional[Any] = None
-
- model_config = ConfigDict(json_encoders={Any: to_python_primitive})
-
-
-class EnvCreated(BaseModel):
- env_id: str
- message: str = "Environment successfully created"
-
-
-class EnvUpdated(BaseModel):
- env_id: str
- message: str = "Environment successfully updated"
-
-
-class EnvDeleted(BaseModel):
- env_id: str
- message: str = "Environment successfully deleted"
diff --git a/lib/views/flight.py b/lib/views/flight.py
deleted file mode 100644
index f06cae4..0000000
--- a/lib/views/flight.py
+++ /dev/null
@@ -1,173 +0,0 @@
-from typing import Optional, Any
-from pydantic import BaseModel, ConfigDict
-from lib.models.flight import Flight
-from lib.views.rocket import RocketView, RocketSummary
-from lib.views.environment import EnvSummary
-from lib.utils import to_python_primitive
-
-
-class FlightSummary(RocketSummary, EnvSummary):
- name: Optional[str] = None
- max_time: Optional[int] = None
- min_time_step: Optional[int] = None
- max_time_step: Optional[Any] = None
- equations_of_motion: Optional[str] = None
- heading: Optional[int] = None
- inclination: Optional[int] = None
- initial_solution: Optional[list] = None
- effective_1rl: Optional[float] = None
- effective_2rl: Optional[float] = None
- out_of_rail_time: Optional[float] = None
- out_of_rail_time_index: Optional[int] = None
- parachute_cd_s: Optional[float] = None
- rail_length: Optional[float] = None
- rtol: Optional[float] = None
- t: Optional[float] = None
- t_final: Optional[float] = None
- t_initial: Optional[int] = None
- terminate_on_apogee: Optional[bool] = None
- time_overshoot: Optional[bool] = None
- latitude: Optional[Any] = None
- longitude: Optional[Any] = None
- M1: Optional[Any] = None
- M2: Optional[Any] = None
- M3: Optional[Any] = None
- R1: Optional[Any] = None
- R2: Optional[Any] = None
- R3: Optional[Any] = None
- acceleration: Optional[Any] = None
- aerodynamic_bending_moment: Optional[Any] = None
- aerodynamic_drag: Optional[Any] = None
- aerodynamic_lift: Optional[Any] = None
- aerodynamic_spin_moment: Optional[Any] = None
- alpha1: Optional[Any] = None
- alpha2: Optional[Any] = None
- alpha3: Optional[Any] = None
- altitude: Optional[Any] = None
- angle_of_attack: Optional[Any] = None
- apogee: Optional[Any] = None
- apogee_freestream_speed: Optional[Any] = None
- apogee_state: Optional[Any] = None
- apogee_time: Optional[Any] = None
- apogee_x: Optional[Any] = None
- apogee_y: Optional[Any] = None
- atol: Optional[Any] = None
- attitude_angle: Optional[Any] = None
- attitude_frequency_response: Optional[Any] = None
- attitude_vector_x: Optional[Any] = None
- attitude_vector_y: Optional[Any] = None
- attitude_vector_z: Optional[Any] = None
- ax: Optional[Any] = None
- ay: Optional[Any] = None
- az: Optional[Any] = None
- bearing: Optional[Any] = None
- drag_power: Optional[Any] = None
- drift: Optional[Any] = None
- dynamic_pressure: Optional[Any] = None
- e0: Optional[Any] = None
- e1: Optional[Any] = None
- e2: Optional[Any] = None
- e3: Optional[Any] = None
- free_stream_speed: Optional[Any] = None
- frontal_surface_wind: Optional[Any] = None
- function_evaluations: Optional[Any] = None
- function_evaluations_per_time_step: Optional[Any] = None
- horizontal_speed: Optional[Any] = None
- impact_state: Optional[Any] = None
- impact_velocity: Optional[Any] = None
- initial_stability_margin: Optional[Any] = None
- kinetic_energy: Optional[Any] = None
- lateral_attitude_angle: Optional[Any] = None
- lateral_surface_wind: Optional[Any] = None
- mach_number: Optional[Any] = None
- max_acceleration: Optional[Any] = None
- max_acceleration_power_off: Optional[Any] = None
- max_acceleration_power_off_time: Optional[Any] = None
- max_acceleration_power_on: Optional[Any] = None
- max_acceleration_power_on_time: Optional[Any] = None
- max_acceleration_time: Optional[Any] = None
- max_dynamic_pressure: Optional[Any] = None
- max_dynamic_pressure_time: Optional[Any] = None
- max_mach_number: Optional[Any] = None
- max_mach_number_time: Optional[Any] = None
- max_rail_button1_normal_force: Optional[Any] = None
- max_rail_button1_shear_force: Optional[Any] = None
- max_rail_button2_normal_force: Optional[Any] = None
- max_rail_button2_shear_force: Optional[Any] = None
- max_reynolds_number: Optional[Any] = None
- max_reynolds_number_time: Optional[Any] = None
- max_speed: Optional[Any] = None
- max_speed_time: Optional[Any] = None
- max_stability_margin: Optional[Any] = None
- max_stability_margin_time: Optional[Any] = None
- max_total_pressure: Optional[Any] = None
- max_total_pressure_time: Optional[Any] = None
- min_stability_margin: Optional[Any] = None
- min_stability_margin_time: Optional[Any] = None
- omega1_frequency_response: Optional[Any] = None
- omega2_frequency_response: Optional[Any] = None
- omega3_frequency_response: Optional[Any] = None
- out_of_rail_stability_margin: Optional[Any] = None
- out_of_rail_state: Optional[Any] = None
- out_of_rail_velocity: Optional[Any] = None
- parachute_events: Optional[Any] = None
- path_angle: Optional[Any] = None
- phi: Optional[Any] = None
- potential_energy: Optional[Any] = None
- psi: Optional[Any] = None
- rail_button1_normal_force: Optional[Any] = None
- rail_button1_shear_force: Optional[Any] = None
- rail_button2_normal_force: Optional[Any] = None
- rail_button2_shear_force: Optional[Any] = None
- reynolds_number: Optional[Any] = None
- rotational_energy: Optional[Any] = None
- solution: Optional[Any] = None
- solution_array: Optional[Any] = None
- speed: Optional[Any] = None
- stability_margin: Optional[Any] = None
- static_margin: Optional[Any] = None
- stream_velocity_x: Optional[Any] = None
- stream_velocity_y: Optional[Any] = None
- stream_velocity_z: Optional[Any] = None
- theta: Optional[Any] = None
- thrust_power: Optional[Any] = None
- time: Optional[Any] = None
- time_steps: Optional[Any] = None
- total_energy: Optional[Any] = None
- total_pressure: Optional[Any] = None
- translational_energy: Optional[Any] = None
- vx: Optional[Any] = None
- vy: Optional[Any] = None
- vz: Optional[Any] = None
- w1: Optional[Any] = None
- w2: Optional[Any] = None
- w3: Optional[Any] = None
- x: Optional[Any] = None
- x_impact: Optional[Any] = None
- y: Optional[Any] = None
- y_impact: Optional[Any] = None
- y_sol: Optional[Any] = None
- z: Optional[Any] = None
- z_impact: Optional[Any] = None
- flight_phases: Optional[Any] = None
-
- model_config = ConfigDict(json_encoders={Any: to_python_primitive})
-
-
-class FlightCreated(BaseModel):
- flight_id: str
- message: str = "Flight successfully created"
-
-
-class FlightUpdated(BaseModel):
- flight_id: str
- message: str = "Flight successfully updated"
-
-
-class FlightDeleted(BaseModel):
- flight_id: str
- message: str = "Flight successfully deleted"
-
-
-class FlightView(Flight):
- rocket: RocketView
diff --git a/lib/views/motor.py b/lib/views/motor.py
deleted file mode 100644
index 858bc0e..0000000
--- a/lib/views/motor.py
+++ /dev/null
@@ -1,91 +0,0 @@
-from typing import List, Any, Optional
-from pydantic import BaseModel, ConfigDict
-from lib.models.motor import Motor, MotorKinds, CoordinateSystemOrientation
-from lib.utils import to_python_primitive
-
-
-class MotorSummary(BaseModel):
- average_thrust: Optional[float] = None
- burn_duration: Optional[float] = None
- burn_out_time: Optional[float] = None
- burn_start_time: Optional[float] = None
- center_of_dry_mass_position: Optional[float] = None
- coordinate_system_orientation: str = (
- CoordinateSystemOrientation.NOZZLE_TO_COMBUSTION_CHAMBER.value
- )
- dry_I_11: Optional[float] = None
- dry_I_12: Optional[float] = None
- dry_I_13: Optional[float] = None
- dry_I_22: Optional[float] = None
- dry_I_23: Optional[float] = None
- dry_I_33: Optional[float] = None
- dry_mass: Optional[float] = None
- grain_burn_out: Optional[float] = None
- grain_density: Optional[float] = None
- grain_initial_height: Optional[float] = None
- grain_initial_inner_radius: Optional[float] = None
- grain_initial_mass: Optional[float] = None
- grain_initial_volume: Optional[float] = None
- grain_number: Optional[int] = None
- grain_outer_radius: Optional[float] = None
- grain_separation: Optional[float] = None
- grains_center_of_mass_position: Optional[float] = None
- interpolate: Optional[str] = None
- max_thrust: Optional[float] = None
- max_thrust_time: Optional[float] = None
- nozzle_position: Optional[float] = None
- nozzle_radius: Optional[float] = None
- propellant_initial_mass: Optional[float] = None
- throat_area: Optional[float] = None
- throat_radius: Optional[float] = None
- thrust_source: Optional[List[List[float]]] = None
- total_impulse: Optional[float] = None
- Kn: Optional[Any] = None
- I_11: Optional[Any] = None
- I_12: Optional[Any] = None
- I_13: Optional[Any] = None
- I_22: Optional[Any] = None
- I_23: Optional[Any] = None
- I_33: Optional[Any] = None
- burn_area: Optional[Any] = None
- burn_rate: Optional[Any] = None
- burn_time: Optional[Any] = None
- center_of_mass: Optional[Any] = None
- center_of_propellant_mass: Optional[Any] = None
- exhaust_velocity: Optional[Any] = None
- grain_height: Optional[Any] = None
- grain_volume: Optional[Any] = None
- grain_inner_radius: Optional[Any] = None
- mass_flow_rate: Optional[Any] = None
- propellant_I_11: Optional[Any] = None
- propellant_I_12: Optional[Any] = None
- propellant_I_13: Optional[Any] = None
- propellant_I_22: Optional[Any] = None
- propellant_I_23: Optional[Any] = None
- propellant_I_33: Optional[Any] = None
- propellant_mass: Optional[Any] = None
- reshape_thrust_curve: Optional[Any] = None
- total_mass: Optional[Any] = None
- total_mass_flow_rate: Optional[Any] = None
- thrust: Optional[Any] = None
-
- model_config = ConfigDict(json_encoders={Any: to_python_primitive})
-
-
-class MotorCreated(BaseModel):
- motor_id: str
- message: str = "Motor successfully created"
-
-
-class MotorUpdated(BaseModel):
- motor_id: str
- message: str = "Motor successfully updated"
-
-
-class MotorDeleted(BaseModel):
- motor_id: str
- message: str = "Motor successfully deleted"
-
-
-class MotorView(Motor):
- selected_motor_kind: MotorKinds
diff --git a/lib/views/rocket.py b/lib/views/rocket.py
deleted file mode 100644
index 45664ad..0000000
--- a/lib/views/rocket.py
+++ /dev/null
@@ -1,60 +0,0 @@
-from typing import Any, Optional
-from pydantic import BaseModel, ConfigDict
-from lib.models.rocket import Rocket, CoordinateSystemOrientation
-from lib.views.motor import MotorView, MotorSummary
-from lib.utils import to_python_primitive
-
-
-class RocketSummary(MotorSummary):
- area: Optional[float] = None
- coordinate_system_orientation: str = (
- CoordinateSystemOrientation.TAIL_TO_NOSE.value
- )
- center_of_mass_without_motor: Optional[float] = None
- motor_center_of_dry_mass_position: Optional[float] = None
- motor_position: Optional[float] = None
- nozzle_position: Optional[float] = None
- nozzle_to_cdm: Optional[float] = None
- cp_eccentricity_x: Optional[float] = None
- cp_eccentricity_y: Optional[float] = None
- thrust_eccentricity_x: Optional[float] = None
- thrust_eccentricity_y: Optional[float] = None
- I_11_without_motor: Optional[Any] = None
- I_12_without_motor: Optional[Any] = None
- I_13_without_motor: Optional[Any] = None
- I_22_without_motor: Optional[Any] = None
- I_23_without_motor: Optional[Any] = None
- I_33_without_motor: Optional[Any] = None
- check_parachute_trigger: Optional[Any] = None
- com_to_cdm_function: Optional[Any] = None
- cp_position: Optional[Any] = None
- motor_center_of_mass_position: Optional[Any] = None
- nozzle_gyration_tensor: Optional[Any] = None
- power_off_drag: Optional[Any] = None
- power_on_drag: Optional[Any] = None
- reduced_mass: Optional[Any] = None
- stability_margin: Optional[Any] = None
- static_margin: Optional[Any] = None
- thrust_to_weight: Optional[Any] = None
- total_lift_coeff_der: Optional[Any] = None
-
- model_config = ConfigDict(json_encoders={Any: to_python_primitive})
-
-
-class RocketCreated(BaseModel):
- rocket_id: str
- message: str = "Rocket successfully created"
-
-
-class RocketUpdated(BaseModel):
- rocket_id: str
- message: str = "Rocket successfully updated"
-
-
-class RocketDeleted(BaseModel):
- rocket_id: str
- message: str = "Rocket successfully deleted"
-
-
-class RocketView(Rocket):
- motor: MotorView
diff --git a/pyproject.toml b/pyproject.toml
index eb8954a..a88cbc7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -23,6 +23,11 @@ Documentation = "http://api.rocketpy.org/docs"
Repository = "https://github.com/RocketPy-Team/infinity-api"
"Bug Tracker" = "https://github.com/RocketPy-Team/Infinity-API/issues"
+[tool.pytest.ini_options]
+pythonpath = ["."]
+testpaths = ["tests"]
+addopts = "--import-mode=importlib"
+
[tool.black]
line-length = 79
include = '\.py$'
@@ -47,6 +52,9 @@ disable = """
too-many-arguments,
redefined-outer-name,
too-many-positional-arguments,
+ no-member,
+ protected-access,
+ import-outside-toplevel,
"""
[tool.ruff]
@@ -55,7 +63,7 @@ target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "N", "Q"]
-ignore = ["N815", "E501", "Q000", "E402"]
+ignore = ["N815", "E501", "Q000", "E402", "N802"]
fixable = [
"F401",
]
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 9b83bee..a20dfb7 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,5 +1,7 @@
+pytest_asyncio
flake8
pylint
ruff
pytest
httpx
+black
diff --git a/requirements.txt b/requirements.txt
index 8d7b739..336559c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,6 +2,7 @@ motor
dill
python-dotenv
fastapi
+uvloop
pydantic
numpy==1.26.4
pymongo
diff --git a/lib/__init__.py b/src/__init__.py
similarity index 89%
rename from lib/__init__.py
rename to src/__init__.py
index 4eecb2e..38835c1 100644
--- a/lib/__init__.py
+++ b/src/__init__.py
@@ -1,4 +1,4 @@
-# lib/__init__.py
+# src.__init__.py
import logging
import sys
@@ -25,6 +25,6 @@ def parse_error(error):
return f"{exc_type}: {exc_obj}"
-from lib.api import ( # pylint: disable=wrong-import-position,cyclic-import,useless-import-alias
+from src.api import ( # pylint: disable=wrong-import-position,cyclic-import,useless-import-alias
app as app,
)
diff --git a/lib/__main__.py b/src/__main__.py
similarity index 77%
rename from lib/__main__.py
rename to src/__main__.py
index 38e9fef..a6937c4 100644
--- a/lib/__main__.py
+++ b/src/__main__.py
@@ -1,5 +1,5 @@
# __main__.py
-from lib.api import app
+from src.api import app
if __name__ == '__main__':
app.run() # pylint: disable=no-member
diff --git a/lib/api.py b/src/api.py
similarity index 87%
rename from lib/api.py
rename to src/api.py
index 91afdb6..5b97be0 100644
--- a/lib/api.py
+++ b/src/api.py
@@ -1,19 +1,14 @@
-"""
-This is the main API file for the RocketPy API.
-"""
-
from fastapi import FastAPI, Request, status
from fastapi.exceptions import RequestValidationError
-from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.utils import get_openapi
from fastapi.responses import RedirectResponse, JSONResponse
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
-from lib import logger, parse_error
-from lib.routes import flight, environment, motor, rocket
-from lib.utils import RocketPyGZipMiddleware
+from src import logger, parse_error
+from src.routes import flight, environment, motor, rocket
+from src.utils import RocketPyGZipMiddleware
app = FastAPI(
swagger_ui_parameters={
@@ -21,14 +16,6 @@
"syntaxHighlight.theme": "obsidian",
}
)
-
-app.add_middleware(
- CORSMiddleware,
- allow_origins=["*"],
- allow_credentials=True,
- allow_methods=["*"],
- allow_headers=["*"],
-)
app.include_router(flight.router)
app.include_router(environment.router)
app.include_router(motor.router)
@@ -46,7 +33,7 @@ def custom_openapi():
return app.openapi_schema
openapi_schema = get_openapi(
title="RocketPy Infinity-API",
- version="2.2.0",
+ version="3.0.0",
description=(
"
RocketPy Infinity-API is a RESTful Open API for RocketPy, a rocket flight simulator.
"
"
"
@@ -87,7 +74,7 @@ async def __perform_healthcheck():
return {"health": "Everything OK!"}
-# Errors
+# Global exception handler
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(
request: Request, exc: RequestValidationError
diff --git a/src/controllers/__init__.py b/src/controllers/__init__.py
new file mode 100644
index 0000000..9eaeadf
--- /dev/null
+++ b/src/controllers/__init__.py
@@ -0,0 +1 @@
+# src.controllers/__init__.py
diff --git a/src/controllers/environment.py b/src/controllers/environment.py
new file mode 100644
index 0000000..3b5b075
--- /dev/null
+++ b/src/controllers/environment.py
@@ -0,0 +1,65 @@
+from src.controllers.interface import (
+ ControllerBase,
+ controller_exception_handler,
+)
+from src.views.environment import EnvironmentSimulation
+from src.models.environment import EnvironmentModel
+from src.services.environment import EnvironmentService
+
+
+class EnvironmentController(ControllerBase):
+ """
+ Controller for the Environment model.
+
+ Enables:
+ - Simulation of a RocketPy Environment.
+ - CRUD for Environment BaseApiModel.
+ """
+
+ def __init__(self):
+ super().__init__(models=[EnvironmentModel])
+
+ @controller_exception_handler
+ async def get_rocketpy_environment_binary(
+ self,
+ env_id: str,
+ ) -> bytes:
+ """
+ Get rocketpy.Environmnet dill binary.
+
+ Args:
+ env_id: str
+
+ Returns:
+ bytes
+
+ Raises:
+ HTTP 404 Not Found: If the env is not found in the database.
+ """
+ env_retrieved = await self.get_environment_by_id(env_id)
+ env_service = EnvironmentService.from_env_model(
+ env_retrieved.environment
+ )
+ return env_service.get_environment_binary()
+
+ @controller_exception_handler
+ async def get_environment_simulation(
+ self, env_id: str
+ ) -> EnvironmentSimulation:
+ """
+ Simulate a rocket environment.
+
+ Args:
+ env_id: str.
+
+ Returns:
+ EnvironmentSimulation
+
+ Raises:
+ HTTP 404 Not Found: If the env does not exist in the database.
+ """
+ env_retrieved = await self.get_environment_by_id(env_id)
+ env_service = EnvironmentService.from_env_model(
+ env_retrieved.environment
+ )
+ return env_service.get_environment_simulation()
diff --git a/src/controllers/flight.py b/src/controllers/flight.py
new file mode 100644
index 0000000..ac8624a
--- /dev/null
+++ b/src/controllers/flight.py
@@ -0,0 +1,112 @@
+from src.controllers.interface import (
+ ControllerBase,
+ controller_exception_handler,
+)
+from src.views.flight import FlightSimulation
+from src.models.flight import FlightModel
+from src.models.environment import EnvironmentModel
+from src.models.rocket import RocketModel
+from src.services.flight import FlightService
+
+
+class FlightController(ControllerBase):
+ """
+ Controller for the Flight model.
+
+ Enables:
+ - Simulation of a RocketPy Flight.
+ - CRUD for Flight BaseApiModel.
+ """
+
+ def __init__(self):
+ super().__init__(models=[FlightModel])
+
+ @controller_exception_handler
+ async def update_environment_by_flight_id(
+ self, flight_id: str, *, environment: EnvironmentModel
+ ) -> None:
+ """
+ Update a models.Flight.environment in the database.
+
+ Args:
+ flight_id: str
+ environment: models.Environment
+
+ Returns:
+ None
+
+ Raises:
+ HTTP 404 Not Found: If the flight is not found in the database.
+ """
+ flight = await self.get_flight_by_id(flight_id)
+ flight.environment = environment
+ await self.update_flight_by_id(flight_id, flight)
+ return
+
+ @controller_exception_handler
+ async def update_rocket_by_flight_id(
+ self, flight_id: str, *, rocket: RocketModel
+ ) -> None:
+ """
+ Update a models.Flight.rocket in the database.
+
+ Args:
+ flight_id: str
+ rocket: models.Rocket
+
+ Returns:
+ None
+
+ Raises:
+ HTTP 404 Not Found: If the flight is not found in the database.
+ """
+ flight = await self.get_flight_by_id(flight_id)
+ flight.rocket = rocket
+ await self.update_flight_by_id(flight_id, flight)
+ return
+
+ @controller_exception_handler
+ async def get_rocketpy_flight_binary(
+ self,
+ flight_id: str,
+ ) -> bytes:
+ """
+ Get rocketpy.flight as dill binary.
+
+ Args:
+ flight_id: str
+
+ Returns:
+ bytes
+
+ Raises:
+ HTTP 404 Not Found: If the flight is not found in the database.
+ """
+ flight_retrieved = await self.get_flight_by_id(flight_id)
+ flight_service = FlightService.from_flight_model(
+ flight_retrieved.flight
+ )
+ return flight_service.get_flight_binary()
+
+ @controller_exception_handler
+ async def get_flight_simulation(
+ self,
+ flight_id: str,
+ ) -> FlightSimulation:
+ """
+ Simulate a rocket flight.
+
+ Args:
+ flight_id: str
+
+ Returns:
+ Flight simulation view.
+
+ Raises:
+ HTTP 404 Not Found: If the flight does not exist in the database.
+ """
+ flight_retrieved = await self.get_flight_by_id(flight_id=flight_id)
+ flight_service = FlightService.from_flight_model(
+ flight_retrieved.flight
+ )
+ return flight_service.get_flight_simulation()
diff --git a/src/controllers/interface.py b/src/controllers/interface.py
new file mode 100644
index 0000000..a3a6975
--- /dev/null
+++ b/src/controllers/interface.py
@@ -0,0 +1,137 @@
+import functools
+from typing import List
+from pymongo.errors import PyMongoError
+from fastapi import HTTPException, status
+
+from src import logger
+from src.models.interface import ApiBaseModel
+from src.views.interface import ApiBaseView
+from src.repositories.interface import RepositoryInterface
+
+
+def controller_exception_handler(method):
+ @functools.wraps(method)
+ async def wrapper(self, *args, **kwargs):
+ try:
+ return await method(self, *args, **kwargs)
+ except PyMongoError:
+ logger.error(f"{method.__name__}: PyMongoError")
+ raise HTTPException(
+ status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
+ detail="Operation failed, please try again later",
+ )
+ except HTTPException as e:
+ raise e from e
+ except Exception as e:
+ logger.exception(f"{method.__name__}: Unexpected error {e}")
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail="Unexpected error",
+ )
+ finally:
+ logger.info(
+ f"Call to {method.__name__} completed for {self.__class__.__name__}"
+ )
+
+ return wrapper
+
+
+class ControllerBase:
+ """
+ ControllerBase is a base class for all controllers. It provides a set of
+ methods to handle the CRUD operations for the models provided in the
+ constructor. CRUD methods are generated dynamically based on the model
+ methods and the model name.
+
+ The methods are named as follows:
+ - post_{model_name} for POST method
+ - get_{model_name}_by_id for GET method
+ - put_{model_name}_by_id for PUT method
+ - delete_{model_name}_by_id for DELETE method
+
+ """
+
+ def __init__(self, models: List[ApiBaseModel]):
+ self._initialized_models = {}
+ self._load_models(models)
+
+ def _load_models(self, models: List[ApiBaseModel]):
+ for model in models:
+ self._initialized_models[model.NAME] = model
+
+ for action in model.METHODS:
+ method_name = (
+ f"{action.lower()}_{model.NAME}"
+ if action == "POST"
+ else f"{action.lower()}_{model.NAME}_by_id"
+ )
+ method = self._generate_method(action.lower(), model)
+ setattr(self, method_name, method)
+
+ def _generate_method(self, action: str, model: ApiBaseModel):
+ async def method(*args, **kwargs):
+ handler = getattr(self, f"_{action}_model", None)
+ if handler:
+ model_repo = RepositoryInterface.get_model_repo(model)
+ return await handler( # pylint: disable=not-callable
+ model, model_repo, *args, **kwargs
+ )
+ raise NotImplementedError(f"_{action}_model is not implemented")
+
+ return method
+
+ @controller_exception_handler
+ async def _post_model(
+ self,
+ model: ApiBaseModel,
+ model_repo: RepositoryInterface,
+ model_instance: ApiBaseModel,
+ ) -> ApiBaseView:
+ async with model_repo() as repo:
+ inserted_id = await getattr(repo, f'create_{model.NAME}')(
+ model_instance
+ )
+ return model.CREATED(inserted_id)
+
+ @controller_exception_handler
+ async def _get_model(
+ self,
+ model: ApiBaseModel,
+ model_repo: RepositoryInterface,
+ model_id: str,
+ ) -> ApiBaseView:
+ async with model_repo() as repo:
+ model_instance = await getattr(repo, f'read_{model.NAME}_by_id')(
+ model_id
+ )
+ if model_instance:
+ return model.RETRIEVED(model_instance)
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=f"{model.NAME} not found",
+ )
+
+ @controller_exception_handler
+ async def _put_model(
+ self,
+ model: ApiBaseModel,
+ model_repo: RepositoryInterface,
+ model_id: str,
+ model_instance: ApiBaseModel,
+ ) -> ApiBaseView:
+ async with model_repo() as repo:
+ await getattr(repo, f'update_{model.NAME}_by_id')(
+ model_id, model_instance
+ )
+ return model.UPDATED()
+
+ @controller_exception_handler
+ async def _delete_model(
+ self,
+ model: ApiBaseModel,
+ model_repo: RepositoryInterface,
+ model_id: str,
+ ) -> ApiBaseView:
+ async with model_repo() as repo:
+ await getattr(repo, f'delete_{model.NAME}_by_id')(model_id)
+ return model.DELETED()
diff --git a/src/controllers/motor.py b/src/controllers/motor.py
new file mode 100644
index 0000000..e446cef
--- /dev/null
+++ b/src/controllers/motor.py
@@ -0,0 +1,59 @@
+from src.controllers.interface import (
+ ControllerBase,
+ controller_exception_handler,
+)
+from src.views.motor import MotorSimulation
+from src.models.motor import MotorModel
+from src.services.motor import MotorService
+
+
+class MotorController(ControllerBase):
+ """
+ Controller for the motor model.
+
+ Enables:
+ - Simulation of a RocketPy Motor.
+ - CRUD for Motor BaseApiModel.
+ """
+
+ def __init__(self):
+ super().__init__(models=[MotorModel])
+
+ @controller_exception_handler
+ async def get_rocketpy_motor_binary(
+ self,
+ motor_id: str,
+ ) -> bytes:
+ """
+ Get a rocketpy.Motor object as a dill binary.
+
+ Args:
+ motor_id: str
+
+ Returns:
+ bytes
+
+ Raises:
+ HTTP 404 Not Found: If the motor is not found in the database.
+ """
+ motor_retrieved = await self.get_motor_by_id(motor_id)
+ motor_service = MotorService.from_motor_model(motor_retrieved.motor)
+ return motor_service.get_motor_binary()
+
+ @controller_exception_handler
+ async def get_motor_simulation(self, motor_id: str) -> MotorSimulation:
+ """
+ Simulate a rocketpy motor.
+
+ Args:
+ motor_id: str
+
+ Returns:
+ views.MotorSimulation
+
+ Raises:
+ HTTP 404 Not Found: If the motor does not exist in the database.
+ """
+ motor_retrieved = await self.get_motor_by_id(motor_id)
+ motor_service = MotorService.from_motor_model(motor_retrieved.motor)
+ return motor_service.get_motor_simulation()
diff --git a/src/controllers/rocket.py b/src/controllers/rocket.py
new file mode 100644
index 0000000..a7dcb4d
--- /dev/null
+++ b/src/controllers/rocket.py
@@ -0,0 +1,63 @@
+from src.controllers.interface import (
+ ControllerBase,
+ controller_exception_handler,
+)
+from src.views.rocket import RocketSimulation
+from src.models.rocket import RocketModel
+from src.services.rocket import RocketService
+
+
+class RocketController(ControllerBase):
+ """
+ Controller for the Rocket model.
+
+ Enables:
+ - Simulation of a RocketPy Rocket.
+ - CRUD for Rocket BaseApiModel.
+ """
+
+ def __init__(self):
+ super().__init__(models=[RocketModel])
+
+ @controller_exception_handler
+ async def get_rocketpy_rocket_binary(self, rocket_id: str) -> bytes:
+ """
+ Get a rocketpy.Rocket object as dill binary.
+
+ Args:
+ rocket_id: str
+
+ Returns:
+ bytes
+
+ Raises:
+ HTTP 404 Not Found: If the rocket is not found in the database.
+ """
+ rocket_retrieved = await self.get_rocket_by_id(rocket_id)
+ rocket_service = RocketService.from_rocket_model(
+ rocket_retrieved.rocket
+ )
+ return rocket_service.get_rocket_binary()
+
+ @controller_exception_handler
+ async def get_rocket_simulation(
+ self,
+ rocket_id: str,
+ ) -> RocketSimulation:
+ """
+ Simulate a rocketpy rocket.
+
+ Args:
+ rocket_id: str
+
+ Returns:
+ views.RocketSimulation
+
+ Raises:
+ HTTP 404 Not Found: If the rocket does not exist in the database.
+ """
+ rocket_retrieved = await self.get_rocket_by_id(rocket_id)
+ rocket_service = RocketService.from_rocket_model(
+ rocket_retrieved.rocket
+ )
+ return rocket_service.get_rocket_simulation()
diff --git a/src/models/environment.py b/src/models/environment.py
new file mode 100644
index 0000000..89315d2
--- /dev/null
+++ b/src/models/environment.py
@@ -0,0 +1,50 @@
+import datetime
+from typing import Optional, ClassVar, Self, Literal
+from src.models.interface import ApiBaseModel
+
+
+class EnvironmentModel(ApiBaseModel):
+ NAME: ClassVar = 'environment'
+ METHODS: ClassVar = ('POST', 'GET', 'PUT', 'DELETE')
+ latitude: float
+ longitude: float
+ elevation: Optional[float] = 0.0
+
+ # Optional parameters
+ atmospheric_model_type: Literal[
+ 'standard_atmosphere',
+ 'custom_atmosphere',
+ 'wyoming_sounding',
+ 'forecast',
+ 'reanalysis',
+ 'ensemble',
+ ] = 'standard_atmosphere'
+ atmospheric_model_file: Optional[str] = None
+ date: Optional[datetime.datetime] = (
+ datetime.datetime.today() + datetime.timedelta(days=1)
+ )
+
+ @staticmethod
+ def UPDATED():
+ return
+
+ @staticmethod
+ def DELETED():
+ return
+
+ @staticmethod
+ def CREATED(model_id: str):
+ from src.views.environment import EnvironmentCreated
+
+ return EnvironmentCreated(environment_id=model_id)
+
+ @staticmethod
+ def RETRIEVED(model_instance: type(Self)):
+ from src.views.environment import EnvironmentRetrieved, EnvironmentView
+
+ return EnvironmentRetrieved(
+ environment=EnvironmentView(
+ environment_id=model_instance.get_id(),
+ **model_instance.model_dump(),
+ )
+ )
diff --git a/src/models/flight.py b/src/models/flight.py
new file mode 100644
index 0000000..44ac16a
--- /dev/null
+++ b/src/models/flight.py
@@ -0,0 +1,70 @@
+from typing import Optional, Self, ClassVar, Literal
+from src.models.interface import ApiBaseModel
+from src.models.rocket import RocketModel
+from src.models.environment import EnvironmentModel
+
+
+class FlightModel(ApiBaseModel):
+ NAME: ClassVar = "flight"
+ METHODS: ClassVar = ("POST", "GET", "PUT", "DELETE")
+
+ name: str = "flight"
+ environment: EnvironmentModel
+ rocket: RocketModel
+ rail_length: float = 1
+ time_overshoot: bool = True
+ terminate_on_apogee: bool = False
+ equations_of_motion: Literal['standard', 'solid_propulsion'] = 'standard'
+
+ # Optional parameters
+ inclination: float = 90.0
+ heading: float = 0.0
+ # TODO: implement initial_solution
+ max_time: Optional[int] = None
+ max_time_step: Optional[float] = None
+ min_time_step: Optional[int] = None
+ rtol: Optional[float] = None
+ atol: Optional[float] = None
+ verbose: Optional[bool] = None
+
+ def get_additional_parameters(self):
+ return {
+ key: value
+ for key, value in self.dict().items()
+ if value is not None
+ and key
+ not in [
+ "name",
+ "environment",
+ "rocket",
+ "rail_length",
+ "time_overshoot",
+ "terminate_on_apogee",
+ "equations_of_motion",
+ ]
+ }
+
+ @staticmethod
+ def UPDATED():
+ return
+
+ @staticmethod
+ def DELETED():
+ return
+
+ @staticmethod
+ def CREATED(model_id: str):
+ from src.views.flight import FlightCreated
+
+ return FlightCreated(flight_id=model_id)
+
+ @staticmethod
+ def RETRIEVED(model_instance: type(Self)):
+ from src.views.flight import FlightRetrieved, FlightView
+
+ return FlightRetrieved(
+ flight=FlightView(
+ flight_id=model_instance.get_id(),
+ **model_instance.model_dump(),
+ )
+ )
diff --git a/src/models/interface.py b/src/models/interface.py
new file mode 100644
index 0000000..bd5b99c
--- /dev/null
+++ b/src/models/interface.py
@@ -0,0 +1,60 @@
+from typing import Self, Optional
+from abc import abstractmethod, ABC
+from pydantic import (
+ BaseModel,
+ PrivateAttr,
+ ConfigDict,
+)
+
+
+class ApiBaseModel(BaseModel, ABC):
+ """
+ Base class for all models in the API.
+
+ This class is used to define the common attributes and
+ methods that all models in the API should have.
+ """
+
+ _id: Optional[str] = PrivateAttr(default=None)
+ model_config = ConfigDict(
+ use_enum_values=True,
+ validate_default=True,
+ validate_all_in_root=True,
+ validate_assignment=True,
+ )
+
+ def set_id(self, value):
+ self._id = value
+
+ def get_id(self):
+ return self._id
+
+ @property
+ @abstractmethod
+ def NAME(): # pylint: disable=invalid-name, no-method-argument
+ pass
+
+ @property
+ @abstractmethod
+ def METHODS(): # pylint: disable=invalid-name, no-method-argument
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def UPDATED(): # pylint: disable=invalid-name
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def DELETED(): # pylint: disable=invalid-name
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def CREATED(model_id: str): # pylint: disable=invalid-name
+ pass
+
+ @staticmethod
+ @abstractmethod
+ def RETRIEVED(model_instance: type(Self)): # pylint: disable=invalid-name
+ pass
diff --git a/src/models/motor.py b/src/models/motor.py
new file mode 100644
index 0000000..4d1cdf5
--- /dev/null
+++ b/src/models/motor.py
@@ -0,0 +1,95 @@
+from enum import Enum
+from typing import Optional, Tuple, List, Union, Self, ClassVar, Literal
+from pydantic import model_validator
+
+from src.models.interface import ApiBaseModel
+from src.models.sub.tanks import MotorTank
+
+
+class MotorKinds(str, Enum):
+ HYBRID: str = "HYBRID"
+ SOLID: str = "SOLID"
+ GENERIC: str = "GENERIC"
+ LIQUID: str = "LIQUID"
+
+
+class MotorModel(ApiBaseModel):
+ NAME: ClassVar = 'motor'
+ METHODS: ClassVar = ('POST', 'GET', 'PUT', 'DELETE')
+
+ # Required parameters
+ thrust_source: List[List[float]]
+ burn_time: float
+ nozzle_radius: float
+ dry_mass: float
+ dry_inertia: Tuple[float, float, float] = (0, 0, 0)
+ center_of_dry_mass_position: float
+ motor_kind: MotorKinds
+
+ # Generic motor parameters
+ chamber_radius: Optional[float] = None
+ chamber_height: Optional[float] = None
+ chamber_position: Optional[float] = None
+ propellant_initial_mass: Optional[float] = None
+ nozzle_position: Optional[float] = None
+
+ # Liquid motor parameters
+ tanks: Optional[List[MotorTank]] = None
+
+ # Solid motor parameters
+ grain_number: Optional[int] = None
+ grain_density: Optional[float] = None
+ grain_outer_radius: Optional[float] = None
+ grain_initial_inner_radius: Optional[float] = None
+ grain_initial_height: Optional[float] = None
+ grains_center_of_mass_position: Optional[float] = None
+ grain_separation: Optional[float] = None
+
+ # Hybrid motor parameters
+ throat_radius: Optional[float] = None
+
+ # Optional parameters
+ interpolation_method: Literal[
+ 'linear', 'spline', 'akima', 'polynomial', 'shepard', 'rbf'
+ ] = 'linear'
+ coordinate_system_orientation: Literal[
+ 'nozzle_to_combustion_chamber', 'combustion_chamber_to_nozzle'
+ ] = 'nozzle_to_combustion_chamber'
+ reshape_thrust_curve: Union[bool, tuple] = False
+
+ @model_validator(mode='after')
+ # TODO: extend guard to check motor kinds and tank kinds specifics
+ def validate_motor_kind(self):
+ if (
+ self.motor_kind not in (MotorKinds.SOLID, MotorKinds.GENERIC)
+ and self.tanks is None
+ ):
+ raise ValueError(
+ "Tanks must be provided for liquid and hybrid motors."
+ )
+ return self
+
+ @staticmethod
+ def UPDATED():
+ return
+
+ @staticmethod
+ def DELETED():
+ return
+
+ @staticmethod
+ def CREATED(model_id: str):
+ from src.views.motor import MotorCreated
+
+ return MotorCreated(motor_id=model_id)
+
+ @staticmethod
+ def RETRIEVED(model_instance: type(Self)):
+ from src.views.motor import MotorRetrieved, MotorView
+
+ return MotorRetrieved(
+ motor=MotorView(
+ motor_id=model_instance.get_id(),
+ **model_instance.model_dump(),
+ )
+ )
diff --git a/src/models/rocket.py b/src/models/rocket.py
new file mode 100644
index 0000000..c2f53a3
--- /dev/null
+++ b/src/models/rocket.py
@@ -0,0 +1,63 @@
+from typing import Optional, Tuple, List, Union, Self, ClassVar, Literal
+from src.models.interface import ApiBaseModel
+from src.models.motor import MotorModel
+from src.models.sub.aerosurfaces import (
+ Fins,
+ NoseCone,
+ Tail,
+ RailButtons,
+ Parachute,
+)
+
+
+class RocketModel(ApiBaseModel):
+ NAME: ClassVar = "rocket"
+ METHODS: ClassVar = ("POST", "GET", "PUT", "DELETE")
+
+ # Required parameters
+ motor: MotorModel
+ radius: float
+ mass: float
+ motor_position: float
+ center_of_mass_without_motor: int
+ inertia: Union[
+ Tuple[float, float, float],
+ Tuple[float, float, float, float, float, float],
+ ] = (0, 0, 0)
+ power_off_drag: List[Tuple[float, float]] = [(0, 0)]
+ power_on_drag: List[Tuple[float, float]] = [(0, 0)]
+ coordinate_system_orientation: Literal['tail_to_nose', 'nose_to_tail'] = (
+ 'tail_to_nose'
+ )
+ nose: NoseCone
+ fins: List[Fins]
+
+ # Optional parameters
+ parachutes: Optional[List[Parachute]] = None
+ rail_buttons: Optional[RailButtons] = None
+ tail: Optional[Tail] = None
+
+ @staticmethod
+ def UPDATED():
+ return
+
+ @staticmethod
+ def DELETED():
+ return
+
+ @staticmethod
+ def CREATED(model_id: str):
+ from src.views.rocket import RocketCreated
+
+ return RocketCreated(rocket_id=model_id)
+
+ @staticmethod
+ def RETRIEVED(model_instance: type(Self)):
+ from src.views.rocket import RocketRetrieved, RocketView
+
+ return RocketRetrieved(
+ rocket=RocketView(
+ rocket_id=model_instance.get_id(),
+ **model_instance.model_dump(),
+ )
+ )
diff --git a/lib/models/aerosurfaces.py b/src/models/sub/aerosurfaces.py
similarity index 79%
rename from lib/models/aerosurfaces.py
rename to src/models/sub/aerosurfaces.py
index 68d8e41..966770e 100644
--- a/lib/models/aerosurfaces.py
+++ b/src/models/sub/aerosurfaces.py
@@ -1,5 +1,4 @@
-from enum import Enum
-from typing import Optional, Tuple, List, Union
+from typing import Optional, Tuple, List, Union, Literal
from pydantic import BaseModel, Field
@@ -28,18 +27,8 @@ class NoseCone(BaseModel):
rocket_radius: float
-class FinsKinds(str, Enum):
- TRAPEZOIDAL: str = "TRAPEZOIDAL"
- ELLIPTICAL: str = "ELLIPTICAL"
-
-
-class AngleUnit(str, Enum):
- RADIANS: str = "RADIANS"
- DEGREES: str = "DEGREES"
-
-
class Fins(BaseModel):
- fins_kind: FinsKinds
+ fins_kind: Literal['trapezoidal', 'elliptical']
name: str
n: int
root_chord: float
@@ -50,7 +39,9 @@ class Fins(BaseModel):
tip_chord: Optional[float] = None
cant_angle: Optional[float] = None
rocket_radius: Optional[float] = None
- airfoil: Optional[Tuple[List[Tuple[float, float]], AngleUnit]] = None
+ airfoil: Optional[
+ Tuple[List[Tuple[float, float]], Literal['radians', 'degrees']]
+ ] = None
def get_additional_parameters(self):
return {
diff --git a/src/models/sub/tanks.py b/src/models/sub/tanks.py
new file mode 100644
index 0000000..0873264
--- /dev/null
+++ b/src/models/sub/tanks.py
@@ -0,0 +1,49 @@
+from enum import Enum
+from typing import Optional, Tuple, List
+from pydantic import BaseModel
+
+
+class TankKinds(str, Enum):
+ LEVEL: str = "LEVEL"
+ MASS: str = "MASS"
+ MASS_FLOW: str = "MASS_FLOW"
+ ULLAGE: str = "ULLAGE"
+
+
+class TankFluids(BaseModel):
+ name: str
+ density: float
+
+
+class MotorTank(BaseModel):
+ # Required parameters
+ geometry: List[Tuple[Tuple[float, float], float]]
+ gas: TankFluids
+ liquid: TankFluids
+ flux_time: Tuple[float, float]
+ position: float
+ discretize: int
+
+ # Level based tank parameters
+ liquid_height: Optional[float] = None
+
+ # Mass based tank parameters
+ liquid_mass: Optional[float] = None
+ gas_mass: Optional[float] = None
+
+ # Mass flow based tank parameters
+ gas_mass_flow_rate_in: Optional[float] = None
+ gas_mass_flow_rate_out: Optional[float] = None
+ liquid_mass_flow_rate_in: Optional[float] = None
+ liquid_mass_flow_rate_out: Optional[float] = None
+ initial_liquid_mass: Optional[float] = None
+ initial_gas_mass: Optional[float] = None
+
+ # Ullage based tank parameters
+ ullage: Optional[float] = None
+
+ # Optional parameters
+ name: Optional[str] = None
+
+ # Computed parameters
+ tank_kind: TankKinds = TankKinds.MASS_FLOW
diff --git a/src/repositories/__init__.py b/src/repositories/__init__.py
new file mode 100644
index 0000000..41576ce
--- /dev/null
+++ b/src/repositories/__init__.py
@@ -0,0 +1 @@
+# src/controllers/__init__.py
diff --git a/src/repositories/environment.py b/src/repositories/environment.py
new file mode 100644
index 0000000..6f483b3
--- /dev/null
+++ b/src/repositories/environment.py
@@ -0,0 +1,40 @@
+from typing import Optional
+from src.models.environment import EnvironmentModel
+from src.repositories.interface import (
+ RepositoryInterface,
+ repository_exception_handler,
+)
+
+
+class EnvironmentRepository(RepositoryInterface):
+ """
+ Enables database CRUD operations with models.Environment
+
+ Init Attributes:
+ environment: models.EnvironmentModel
+ """
+
+ def __init__(self):
+ super().__init__(EnvironmentModel)
+
+ @repository_exception_handler
+ async def create_environment(self, environment: EnvironmentModel) -> str:
+ return await self.insert(environment.model_dump())
+
+ @repository_exception_handler
+ async def read_environment_by_id(
+ self, environment_id: str
+ ) -> Optional[EnvironmentModel]:
+ return await self.find_by_id(data_id=environment_id)
+
+ @repository_exception_handler
+ async def update_environment_by_id(
+ self, environment_id: str, environment: EnvironmentModel
+ ):
+ await self.update_by_id(
+ environment.model_dump(), data_id=environment_id
+ )
+
+ @repository_exception_handler
+ async def delete_environment_by_id(self, environment_id: str):
+ await self.delete_by_id(data_id=environment_id)
diff --git a/src/repositories/flight.py b/src/repositories/flight.py
new file mode 100644
index 0000000..3b3448b
--- /dev/null
+++ b/src/repositories/flight.py
@@ -0,0 +1,34 @@
+from typing import Optional
+from src.models.flight import FlightModel
+from src.repositories.interface import (
+ RepositoryInterface,
+ repository_exception_handler,
+)
+
+
+class FlightRepository(RepositoryInterface):
+ """
+ Enables database CRUD operations with models.Flight
+
+ Init Attributes:
+ flight: models.FlightModel
+ """
+
+ def __init__(self):
+ super().__init__(FlightModel)
+
+ @repository_exception_handler
+ async def create_flight(self, flight: FlightModel) -> str:
+ return await self.insert(flight.model_dump())
+
+ @repository_exception_handler
+ async def read_flight_by_id(self, flight_id: str) -> Optional[FlightModel]:
+ return await self.find_by_id(data_id=flight_id)
+
+ @repository_exception_handler
+ async def update_flight_by_id(self, flight_id: str, flight: FlightModel):
+ await self.update_by_id(flight.model_dump(), data_id=flight_id)
+
+ @repository_exception_handler
+ async def delete_flight_by_id(self, flight_id: str):
+ await self.delete_by_id(data_id=flight_id)
diff --git a/lib/repositories/repo.py b/src/repositories/interface.py
similarity index 54%
rename from lib/repositories/repo.py
rename to src/repositories/interface.py
index 9a9f7ae..f5dcc22 100644
--- a/lib/repositories/repo.py
+++ b/src/repositories/interface.py
@@ -1,18 +1,28 @@
+import importlib
import asyncio
import threading
+import functools
+from typing import Self
from tenacity import (
stop_after_attempt,
wait_fixed,
retry,
)
-from pydantic import BaseModel
+from pydantic import BaseModel, ValidationError
+from pymongo.errors import PyMongoError
from pymongo.server_api import ServerApi
from motor.motor_asyncio import AsyncIOMotorClient
from fastapi import HTTPException, status
+from bson import ObjectId
-from lib import logger
-from lib.secrets import Secrets
+from src import logger
+from src.secrets import Secrets
+from src.models.interface import ApiBaseModel
+
+
+def not_implemented(*args, **kwargs):
+ raise NotImplementedError("Method not implemented.")
class RepositoryNotInitializedException(HTTPException):
@@ -23,6 +33,35 @@ def __init__(self):
)
+def repository_exception_handler(method):
+ @functools.wraps(method)
+ async def wrapper(self, *args, **kwargs):
+ try:
+ return await method(self, *args, **kwargs)
+ except PyMongoError as e:
+ logger.exception(f"{method.__name__} - caught PyMongoError: {e}")
+ raise
+ except RepositoryNotInitializedException as e:
+ logger.exception(
+ f"{method.__name__} - Repository not initialized: {e}"
+ )
+ raise
+ except Exception as e:
+ logger.exception(
+ f"{method.__name__} - caught unexpected error: {e}"
+ )
+ raise HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ detail='Unexpected error ocurred',
+ ) from e
+ finally:
+ logger.info(
+ f"Call to repositories.{self.model.NAME}.{method.__name__} completed for {kwargs}"
+ )
+
+ return wrapper
+
+
class RepoInstances(BaseModel):
instance: object
prospecting: int = 0
@@ -40,9 +79,18 @@ def get_instance(self):
return self.instance
-class Repository:
+class RepositoryInterface:
"""
- Base class for all repositories (singleton)
+ Interface class for all repositories (singleton)
+
+ This class is used to define the common attributes and
+ methods that all repositories should have.
+
+ The class is a singleton, meaning that only one instance
+ of the class is created and shared among all instances
+ of the class. This is done to ensure that only one
+ connection per collection in the database is created
+ and shared among all repositories.
"""
_global_instances = {}
@@ -54,7 +102,7 @@ def __new__(cls, *args, **kwargs):
cls._global_thread_lock
): # Ensure thread safety for singleton instance creation
if cls not in cls._global_instances:
- instance = super(Repository, cls).__new__(cls)
+ instance = super(RepositoryInterface, cls).__new__(cls)
cls._global_instances[cls] = RepoInstances(instance=instance)
else:
cls._global_instances[cls].add_prospecting()
@@ -71,10 +119,10 @@ def _get_instance_prospecting(cls):
return cls._global_instances[cls].get_prospecting()
return 0
- def __init__(self, collection_name: str, *, max_pool_size: int = 10):
+ def __init__(self, model: ApiBaseModel, *, max_pool_size: int = 3):
if not getattr(self, '_initialized', False):
+ self.model = model
self._max_pool_size = max_pool_size
- self._collection_name = collection_name
self._initialized_event = asyncio.Event()
self._initialize()
@@ -128,11 +176,11 @@ def _initialize_connection(self):
self._connection_string,
server_api=ServerApi("1"),
maxIdleTimeMS=30000,
- minPoolSize=10,
+ minPoolSize=1,
maxPoolSize=self._max_pool_size,
serverSelectionTimeoutMS=60000,
)
- self._collection = self._client.rocketpy[self._collection_name]
+ self._collection = self._client.rocketpy[self.model.NAME]
logger.info("MongoDB client initialized for %s", self.__class__)
except Exception as e:
logger.error(
@@ -164,3 +212,54 @@ def get_collection(self):
if not getattr(self, '_initialized', False):
raise RepositoryNotInitializedException()
return self._collection
+
+ @classmethod
+ def get_model_repo(cls, model: ApiBaseModel) -> Self:
+ repo_path = cls.__module__.replace('interface', f'{model.NAME}')
+ return getattr(
+ importlib.import_module(repo_path),
+ f"{model.NAME.capitalize()}Repository",
+ )
+
+ @repository_exception_handler
+ async def insert(self, data: dict):
+ collection = self.get_collection()
+ try:
+ self.model.model_validate(data)
+ except ValidationError as e:
+ raise HTTPException(status_code=422, detail=str(e))
+ result = await collection.insert_one(data)
+ return str(result.inserted_id)
+
+ @repository_exception_handler
+ async def update_by_id(self, data: dict, *, data_id: str):
+ collection = self.get_collection()
+ assert self.model.model_validate(data)
+ await collection.update_one({"_id": ObjectId(data_id)}, {"$set": data})
+ return self
+
+ @repository_exception_handler
+ async def find_by_id(self, *, data_id: str):
+ collection = self.get_collection()
+ read_data = await collection.find_one({"_id": ObjectId(data_id)})
+ if read_data:
+ parsed_model = self.model.model_validate(read_data)
+ parsed_model.set_id(str(read_data["_id"]))
+ return parsed_model
+ return None
+
+ @repository_exception_handler
+ async def delete_by_id(self, *, data_id: str):
+ collection = self.get_collection()
+ await collection.delete_one({"_id": ObjectId(data_id)})
+ return self
+
+ @repository_exception_handler
+ async def find_by_query(self, query: dict):
+ collection = self.get_collection()
+ parsed_models = []
+ async for read_data in collection.find(query):
+ parsed_model = self.model.model_validate(read_data)
+ parsed_model.set_id(str(read_data["_id"]))
+ parsed_models.append(parsed_model)
+ return parsed_models
diff --git a/src/repositories/motor.py b/src/repositories/motor.py
new file mode 100644
index 0000000..026655f
--- /dev/null
+++ b/src/repositories/motor.py
@@ -0,0 +1,34 @@
+from typing import Optional
+from src.models.motor import MotorModel
+from src.repositories.interface import (
+ RepositoryInterface,
+ repository_exception_handler,
+)
+
+
+class MotorRepository(RepositoryInterface):
+ """
+ Enables database CRUD operations with models.Motor
+
+ Init Attributes:
+ motor: models.MotorModel
+ """
+
+ def __init__(self):
+ super().__init__(MotorModel)
+
+ @repository_exception_handler
+ async def create_motor(self, motor: MotorModel) -> str:
+ return await self.insert(motor.model_dump())
+
+ @repository_exception_handler
+ async def read_motor_by_id(self, motor_id: str) -> Optional[MotorModel]:
+ return await self.find_by_id(data_id=motor_id)
+
+ @repository_exception_handler
+ async def update_motor_by_id(self, motor_id: str, motor: MotorModel):
+ await self.update_by_id(motor.model_dump(), data_id=motor_id)
+
+ @repository_exception_handler
+ async def delete_motor_by_id(self, motor_id: str):
+ await self.delete_by_id(data_id=motor_id)
diff --git a/src/repositories/rocket.py b/src/repositories/rocket.py
new file mode 100644
index 0000000..f4b6f38
--- /dev/null
+++ b/src/repositories/rocket.py
@@ -0,0 +1,34 @@
+from typing import Optional
+from src.models.rocket import RocketModel
+from src.repositories.interface import (
+ RepositoryInterface,
+ repository_exception_handler,
+)
+
+
+class RocketRepository(RepositoryInterface):
+ """
+ Enables database CRUD operations with models.Rocket
+
+ Init Attributes:
+ rocket: models.RocketModel
+ """
+
+ def __init__(self):
+ super().__init__(RocketModel)
+
+ @repository_exception_handler
+ async def create_rocket(self, rocket: RocketModel) -> str:
+ return await self.insert(rocket.model_dump())
+
+ @repository_exception_handler
+ async def read_rocket_by_id(self, rocket_id: str) -> Optional[RocketModel]:
+ return await self.find_by_id(data_id=rocket_id)
+
+ @repository_exception_handler
+ async def update_rocket_by_id(self, rocket_id: str, rocket: RocketModel):
+ await self.update_by_id(rocket.model_dump(), data_id=rocket_id)
+
+ @repository_exception_handler
+ async def delete_rocket_by_id(self, rocket_id: str):
+ await self.delete_by_id(data_id=rocket_id)
diff --git a/src/routes/environment.py b/src/routes/environment.py
new file mode 100644
index 0000000..6c5e8b2
--- /dev/null
+++ b/src/routes/environment.py
@@ -0,0 +1,137 @@
+"""
+Environment routes
+"""
+
+from fastapi import APIRouter, Response
+from opentelemetry import trace
+
+from src.views.environment import (
+ EnvironmentSimulation,
+ EnvironmentCreated,
+ EnvironmentRetrieved,
+)
+from src.models.environment import EnvironmentModel
+from src.controllers.environment import EnvironmentController
+
+router = APIRouter(
+ prefix="/environments",
+ tags=["ENVIRONMENT"],
+ responses={
+ 404: {"description": "Not found"},
+ 422: {"description": "Unprocessable Entity"},
+ 500: {"description": "Internal Server Error"},
+ },
+)
+
+tracer = trace.get_tracer(__name__)
+
+
+@router.post("/", status_code=201)
+async def create_environment(
+ environment: EnvironmentModel,
+) -> EnvironmentCreated:
+ """
+ Creates a new environment
+
+ ## Args
+ ``` models.Environment JSON ```
+ """
+ with tracer.start_as_current_span("create_environment"):
+ controller = EnvironmentController()
+ return await controller.post_environment(environment)
+
+
+@router.get("/{environment_id}")
+async def read_environment(environment_id: str) -> EnvironmentRetrieved:
+ """
+ Reads an existing environment
+
+ ## Args
+ ``` environment_id: str ```
+ """
+ with tracer.start_as_current_span("read_environment"):
+ controller = EnvironmentController()
+ return await controller.get_environment_by_id(environment_id)
+
+
+@router.put("/{environment_id}", status_code=204)
+async def update_environment(
+ environment_id: str, environment: EnvironmentModel
+) -> None:
+ """
+ Updates an existing environment
+
+ ## Args
+ ```
+ environment_id: str
+ environment: models.Environment JSON
+ ```
+ """
+ with tracer.start_as_current_span("update_environment"):
+ controller = EnvironmentController()
+ return await controller.put_environment_by_id(
+ environment_id, environment
+ )
+
+
+@router.delete("/{environment_id}", status_code=204)
+async def delete_environment(environment_id: str) -> None:
+ """
+ Deletes an existing environment
+
+ ## Args
+ ``` environment_id: str ```
+ """
+ with tracer.start_as_current_span("delete_environment"):
+ controller = EnvironmentController()
+ return await controller.delete_environment_by_id(environment_id)
+
+
+@router.get(
+ "/{environment_id}/rocketpy",
+ responses={
+ 200: {
+ "description": "Binary file download",
+ "content": {"application/octet-stream": {}},
+ }
+ },
+ status_code=200,
+ response_class=Response,
+)
+async def get_rocketpy_environment_binary(environment_id: str):
+ """
+ Loads rocketpy.environment as a dill binary.
+ Currently only amd64 architecture is supported.
+
+ ## Args
+ ``` environment_id: str ```
+ """
+ with tracer.start_as_current_span("get_rocketpy_environment_binary"):
+ headers = {
+ 'Content-Disposition': f'attachment; filename="rocketpy_environment_{environment_id}.dill"'
+ }
+ controller = EnvironmentController()
+ binary = await controller.get_rocketpy_environment_binary(
+ environment_id
+ )
+ return Response(
+ content=binary,
+ headers=headers,
+ media_type="application/octet-stream",
+ status_code=200,
+ )
+
+
+@router.get("/{environment_id}/simulate")
+async def get_environment_simulation(
+ environment_id: str,
+) -> EnvironmentSimulation:
+ """
+ Simulates an environment
+
+ ## Args
+ ``` environment_id: Environment ID```
+ """
+ with tracer.start_as_current_span("get_environment_simulation"):
+ controller = EnvironmentController()
+ return await controller.get_environment_simulation(environment_id)
diff --git a/src/routes/flight.py b/src/routes/flight.py
new file mode 100644
index 0000000..9aff0db
--- /dev/null
+++ b/src/routes/flight.py
@@ -0,0 +1,168 @@
+"""
+Flight routes
+"""
+
+from fastapi import APIRouter, Response
+from opentelemetry import trace
+
+from src.views.flight import (
+ FlightSimulation,
+ FlightCreated,
+ FlightRetrieved,
+)
+from src.models.environment import EnvironmentModel
+from src.models.flight import FlightModel
+from src.models.rocket import RocketModel
+from src.controllers.flight import FlightController
+
+router = APIRouter(
+ prefix="/flights",
+ tags=["FLIGHT"],
+ responses={
+ 404: {"description": "Not found"},
+ 422: {"description": "Unprocessable Entity"},
+ 500: {"description": "Internal Server Error"},
+ },
+)
+
+tracer = trace.get_tracer(__name__)
+
+
+@router.post("/", status_code=201)
+async def create_flight(flight: FlightModel) -> FlightCreated:
+ """
+ Creates a new flight
+
+ ## Args
+ ``` models.Flight JSON ```
+ """
+ with tracer.start_as_current_span("create_flight"):
+ controller = FlightController()
+ return await controller.post_flight(flight)
+
+
+@router.get("/{flight_id}")
+async def read_flight(flight_id: str) -> FlightRetrieved:
+ """
+ Reads an existing flight
+
+ ## Args
+ ``` flight_id: str ```
+ """
+ with tracer.start_as_current_span("read_flight"):
+ controller = FlightController()
+ return await controller.get_flight_by_id(flight_id)
+
+
+@router.put("/{flight_id}", status_code=204)
+async def update_flight(flight_id: str, flight: FlightModel) -> None:
+ """
+ Updates an existing flight
+
+ ## Args
+ ```
+ flight_id: str
+ flight: models.flight JSON
+ ```
+ """
+ with tracer.start_as_current_span("update_flight"):
+ controller = FlightController()
+ return await controller.put_flight_by_id(flight_id, flight)
+
+
+@router.delete("/{flight_id}", status_code=204)
+async def delete_flight(flight_id: str) -> None:
+ """
+ Deletes an existing flight
+
+ ## Args
+ ``` flight_id: str ```
+ """
+ with tracer.start_as_current_span("delete_flight"):
+ controller = FlightController()
+ return await controller.delete_flight_by_id(flight_id)
+
+
+@router.get(
+ "/{flight_id}/rocketpy",
+ responses={
+ 200: {
+ "description": "Binary file download",
+ "content": {"application/octet-stream": {}},
+ }
+ },
+ status_code=200,
+ response_class=Response,
+)
+async def get_rocketpy_flight_binary(flight_id: str):
+ """
+ Loads rocketpy.flight as a dill binary.
+ Currently only amd64 architecture is supported.
+
+ ## Args
+ ``` flight_id: str ```
+ """
+ with tracer.start_as_current_span("get_rocketpy_flight_binary"):
+ controller = FlightController()
+ headers = {
+ 'Content-Disposition': f'attachment; filename="rocketpy_flight_{flight_id}.dill"'
+ }
+ binary = await controller.get_rocketpy_flight_binary(flight_id)
+ return Response(
+ content=binary,
+ headers=headers,
+ media_type="application/octet-stream",
+ status_code=200,
+ )
+
+
+@router.put("/{flight_id}/environment", status_code=204)
+async def update_flight_environment(
+ flight_id: str, environment: EnvironmentModel
+) -> None:
+ """
+ Updates flight environment
+
+ ## Args
+ ```
+ flight_id: Flight ID
+ environment: env object as JSON
+ ```
+ """
+ with tracer.start_as_current_span("update_flight_environment"):
+ controller = FlightController()
+ return await controller.update_environment_by_flight_id(
+ flight_id, environment=environment
+ )
+
+
+@router.put("/{flight_id}/rocket", status_code=204)
+async def update_flight_rocket(flight_id: str, rocket: RocketModel) -> None:
+ """
+ Updates flight rocket.
+
+ ## Args
+ ```
+ flight_id: Flight ID
+ rocket: RocketModel object as JSON
+ ```
+ """
+ with tracer.start_as_current_span("update_flight_rocket"):
+ controller = FlightController()
+ return await controller.update_rocket_by_flight_id(
+ flight_id,
+ rocket=rocket,
+ )
+
+
+@router.get("/{flight_id}/simulate")
+async def get_flight_simulation(flight_id: str) -> FlightSimulation:
+ """
+ Simulates a flight
+
+ ## Args
+ ``` flight_id: Flight ID ```
+ """
+ with tracer.start_as_current_span("get_flight_simulation"):
+ controller = FlightController()
+ return await controller.get_flight_simulation(flight_id)
diff --git a/src/routes/motor.py b/src/routes/motor.py
new file mode 100644
index 0000000..3143c26
--- /dev/null
+++ b/src/routes/motor.py
@@ -0,0 +1,127 @@
+"""
+Motor routes
+"""
+
+from fastapi import APIRouter, Response
+from opentelemetry import trace
+
+from src.views.motor import (
+ MotorSimulation,
+ MotorCreated,
+ MotorRetrieved,
+)
+from src.models.motor import MotorModel
+from src.controllers.motor import MotorController
+
+router = APIRouter(
+ prefix="/motors",
+ tags=["MOTOR"],
+ responses={
+ 404: {"description": "Not found"},
+ 422: {"description": "Unprocessable Entity"},
+ 500: {"description": "Internal Server Error"},
+ },
+)
+
+tracer = trace.get_tracer(__name__)
+
+
+@router.post("/", status_code=201)
+async def create_motor(motor: MotorModel) -> MotorCreated:
+ """
+ Creates a new motor
+
+ ## Args
+ ``` models.Motor JSON ```
+ """
+ with tracer.start_as_current_span("create_motor"):
+ controller = MotorController()
+ return await controller.post_motor(motor)
+
+
+@router.get("/{motor_id}")
+async def read_motor(motor_id: str) -> MotorRetrieved:
+ """
+ Reads an existing motor
+
+ ## Args
+ ``` motor_id: str ```
+ """
+ with tracer.start_as_current_span("read_motor"):
+ controller = MotorController()
+ return await controller.get_motor_by_id(motor_id)
+
+
+@router.put("/{motor_id}", status_code=204)
+async def update_motor(motor_id: str, motor: MotorModel) -> None:
+ """
+ Updates an existing motor
+
+ ## Args
+ ```
+ motor_id: str
+ motor: models.motor JSON
+ ```
+ """
+ with tracer.start_as_current_span("update_motor"):
+ controller = MotorController()
+ return await controller.put_motor_by_id(motor_id, motor)
+
+
+@router.delete("/{motor_id}", status_code=204)
+async def delete_motor(motor_id: str) -> None:
+ """
+ Deletes an existing motor
+
+ ## Args
+ ``` motor_id: str ```
+ """
+ with tracer.start_as_current_span("delete_motor"):
+ controller = MotorController()
+ return await controller.delete_motor_by_id(motor_id)
+
+
+@router.get(
+ "/{motor_id}/rocketpy",
+ responses={
+ 200: {
+ "description": "Binary file download",
+ "content": {"application/octet-stream": {}},
+ }
+ },
+ status_code=200,
+ response_class=Response,
+)
+async def get_rocketpy_motor_binary(motor_id: str):
+ """
+ Loads rocketpy.motor as a dill binary.
+ Currently only amd64 architecture is supported.
+
+ ## Args
+ ``` motor_id: str ```
+ """
+ with tracer.start_as_current_span("get_rocketpy_motor_binary"):
+ headers = {
+ 'Content-Disposition': f'attachment; filename="rocketpy_motor_{motor_id}.dill"'
+ }
+ controller = MotorController()
+ binary = await controller.get_rocketpy_motor_binary(motor_id)
+ return Response(
+ content=binary,
+ headers=headers,
+ media_type="application/octet-stream",
+ status_code=200,
+ )
+
+
+@router.get("/{motor_id}/simulate")
+async def get_motor_simulation(motor_id: str) -> MotorSimulation:
+ """
+ Simulates a motor
+
+ ## Args
+ ``` motor_id: Motor ID ```
+ """
+ with tracer.start_as_current_span("get_motor_simulation"):
+ controller = MotorController()
+ return await controller.get_motor_simulation(motor_id)
diff --git a/src/routes/rocket.py b/src/routes/rocket.py
new file mode 100644
index 0000000..5346d9e
--- /dev/null
+++ b/src/routes/rocket.py
@@ -0,0 +1,127 @@
+"""
+Rocket routes
+"""
+
+from fastapi import APIRouter, Response
+from opentelemetry import trace
+
+from src.views.rocket import (
+ RocketSimulation,
+ RocketCreated,
+ RocketRetrieved,
+)
+from src.models.rocket import RocketModel
+from src.controllers.rocket import RocketController
+
+router = APIRouter(
+ prefix="/rockets",
+ tags=["ROCKET"],
+ responses={
+ 404: {"description": "Not found"},
+ 422: {"description": "Unprocessable Entity"},
+ 500: {"description": "Internal Server Error"},
+ },
+)
+
+tracer = trace.get_tracer(__name__)
+
+
+@router.post("/", status_code=201)
+async def create_rocket(rocket: RocketModel) -> RocketCreated:
+ """
+ Creates a new rocket
+
+ ## Args
+ ``` models.Rocket JSON ```
+ """
+ with tracer.start_as_current_span("create_rocket"):
+ controller = RocketController()
+ return await controller.post_rocket(rocket)
+
+
+@router.get("/{rocket_id}")
+async def read_rocket(rocket_id: str) -> RocketRetrieved:
+ """
+ Reads an existing rocket
+
+ ## Args
+ ``` rocket_id: str ```
+ """
+ with tracer.start_as_current_span("read_rocket"):
+ controller = RocketController()
+ return await controller.get_rocket_by_id(rocket_id)
+
+
+@router.put("/{rocket_id}", status_code=204)
+async def update_rocket(rocket_id: str, rocket: RocketModel) -> None:
+ """
+ Updates an existing rocket
+
+ ## Args
+ ```
+ rocket_id: str
+ rocket: models.rocket JSON
+ ```
+ """
+ with tracer.start_as_current_span("update_rocket"):
+ controller = RocketController()
+ return await controller.put_rocket_by_id(rocket_id, rocket)
+
+
+@router.delete("/{rocket_id}", status_code=204)
+async def delete_rocket(rocket_id: str) -> None:
+ """
+ Deletes an existing rocket
+
+ ## Args
+ ``` rocket_id: str ```
+ """
+ with tracer.start_as_current_span("delete_rocket"):
+ controller = RocketController()
+ return await controller.delete_rocket_by_id(rocket_id)
+
+
+@router.get(
+ "/{rocket_id}/rocketpy",
+ responses={
+ 200: {
+ "description": "Binary file download",
+ "content": {"application/octet-stream": {}},
+ }
+ },
+ status_code=200,
+ response_class=Response,
+)
+async def get_rocketpy_rocket_binary(rocket_id: str):
+ """
+ Loads rocketpy.rocket as a dill binary.
+ Currently only amd64 architecture is supported.
+
+ ## Args
+ ``` rocket_id: str ```
+ """
+ with tracer.start_as_current_span("get_rocketpy_rocket_binary"):
+ headers = {
+ 'Content-Disposition': f'attachment; filename="rocketpy_rocket_{rocket_id}.dill"'
+ }
+ controller = RocketController()
+ binary = await controller.get_rocketpy_rocket_binary(rocket_id)
+ return Response(
+ content=binary,
+ headers=headers,
+ media_type="application/octet-stream",
+ status_code=200,
+ )
+
+
+@router.get("/{rocket_id}/simulate")
+async def simulate_rocket(rocket_id: str) -> RocketSimulation:
+ """
+ Simulates a rocket
+
+ ## Args
+ ``` rocket_id: Rocket ID ```
+ """
+ with tracer.start_as_current_span("get_rocket_simulation"):
+ controller = RocketController()
+ return await controller.get_rocket_simulation(rocket_id)
diff --git a/lib/secrets.py b/src/secrets.py
similarity index 100%
rename from lib/secrets.py
rename to src/secrets.py
diff --git a/lib/services/environment.py b/src/services/environment.py
similarity index 72%
rename from lib/services/environment.py
rename to src/services/environment.py
index f98ea84..4f1137c 100644
--- a/lib/services/environment.py
+++ b/src/services/environment.py
@@ -4,8 +4,8 @@
from rocketpy.environment.environment import Environment as RocketPyEnvironment
from rocketpy.utilities import get_instance_attributes
-from lib.models.environment import Env
-from lib.views.environment import EnvSummary
+from src.models.environment import EnvironmentModel
+from src.views.environment import EnvironmentSimulation
class EnvironmentService:
@@ -15,7 +15,7 @@ def __init__(self, environment: RocketPyEnvironment = None):
self._environment = environment
@classmethod
- def from_env_model(cls, env: Env) -> Self:
+ def from_env_model(cls, env: EnvironmentModel) -> Self:
"""
Get the rocketpy env object.
@@ -29,7 +29,7 @@ def from_env_model(cls, env: Env) -> Self:
date=env.date,
)
rocketpy_env.set_atmospheric_model(
- type=env.atmospheric_model_type.value.lower(),
+ type=env.atmospheric_model_type,
file=env.atmospheric_model_file,
)
return cls(environment=rocketpy_env)
@@ -42,19 +42,19 @@ def environment(self) -> RocketPyEnvironment:
def environment(self, environment: RocketPyEnvironment):
self._environment = environment
- def get_env_summary(self) -> EnvSummary:
+ def get_environment_simulation(self) -> EnvironmentSimulation:
"""
- Get the summary of the environment.
+ Get the simulation of the environment.
Returns:
- EnvSummary
+ EnvironmentSimulation
"""
attributes = get_instance_attributes(self.environment)
- env_summary = EnvSummary(**attributes)
- return env_summary
+ env_simulation = EnvironmentSimulation(**attributes)
+ return env_simulation
- def get_env_binary(self) -> bytes:
+ def get_environment_binary(self) -> bytes:
"""
Get the binary representation of the environment.
diff --git a/lib/services/flight.py b/src/services/flight.py
similarity index 73%
rename from lib/services/flight.py
rename to src/services/flight.py
index 169b9fa..fdce768 100644
--- a/lib/services/flight.py
+++ b/src/services/flight.py
@@ -5,9 +5,10 @@
from rocketpy.simulation.flight import Flight as RocketPyFlight
from rocketpy.utilities import get_instance_attributes
-from lib.services.environment import EnvironmentService
-from lib.services.rocket import RocketService
-from lib.views.flight import FlightSummary, FlightView
+from src.services.environment import EnvironmentService
+from src.services.rocket import RocketService
+from src.models.flight import FlightModel
+from src.views.flight import FlightSimulation
class FlightService:
@@ -17,7 +18,7 @@ def __init__(self, flight: RocketPyFlight = None):
self._flight = flight
@classmethod
- def from_flight_model(cls, flight: FlightView) -> Self:
+ def from_flight_model(cls, flight: FlightModel) -> Self:
"""
Get the rocketpy flight object.
@@ -34,7 +35,7 @@ def from_flight_model(cls, flight: FlightView) -> Self:
rail_length=flight.rail_length,
terminate_on_apogee=flight.terminate_on_apogee,
time_overshoot=flight.time_overshoot,
- equations_of_motion=flight.equations_of_motion.value.lower(),
+ equations_of_motion=flight.equations_of_motion,
**flight.get_additional_parameters(),
)
return cls(flight=rocketpy_flight)
@@ -47,16 +48,16 @@ def flight(self) -> RocketPyFlight:
def flight(self, flight: RocketPyFlight):
self._flight = flight
- def get_flight_summary(self) -> FlightSummary:
+ def get_flight_simulation(self) -> FlightSimulation:
"""
- Get the summary of the flight.
+ Get the simulation of the flight.
Returns:
- FlightSummary
+ FlightSimulation
"""
attributes = get_instance_attributes(self.flight)
- flight_summary = FlightSummary(**attributes)
- return flight_summary
+ flight_simulation = FlightSimulation(**attributes)
+ return flight_simulation
def get_flight_binary(self) -> bytes:
"""
diff --git a/lib/services/motor.py b/src/services/motor.py
similarity index 91%
rename from lib/services/motor.py
rename to src/services/motor.py
index 4bf4b35..5b2acde 100644
--- a/lib/services/motor.py
+++ b/src/services/motor.py
@@ -15,8 +15,9 @@
TankGeometry,
)
-from lib.models.motor import MotorKinds, TankKinds
-from lib.views.motor import MotorSummary, MotorView
+from src.models.sub.tanks import TankKinds
+from src.models.motor import MotorKinds, MotorModel
+from src.views.motor import MotorSimulation
class MotorService:
@@ -26,7 +27,7 @@ def __init__(self, motor: RocketPyMotor = None):
self._motor = motor
@classmethod
- def from_motor_model(cls, motor: MotorView) -> Self:
+ def from_motor_model(cls, motor: MotorModel) -> Self:
"""
Get the rocketpy motor object.
@@ -41,12 +42,12 @@ def from_motor_model(cls, motor: MotorView) -> Self:
"dry_mass": motor.dry_mass,
"dry_inertia": motor.dry_inertia,
"center_of_dry_mass_position": motor.center_of_dry_mass_position,
- "coordinate_system_orientation": motor.coordinate_system_orientation.value.lower(),
- "interpolation_method": motor.interpolation_method.value.lower(),
+ "coordinate_system_orientation": motor.coordinate_system_orientation,
+ "interpolation_method": motor.interpolation_method,
"reshape_thrust_curve": False or motor.reshape_thrust_curve,
}
- match MotorKinds(motor.selected_motor_kind):
+ match MotorKinds(motor.motor_kind):
case MotorKinds.LIQUID:
rocketpy_motor = LiquidMotor(**motor_core)
case MotorKinds.HYBRID:
@@ -132,16 +133,16 @@ def motor(self) -> RocketPyMotor:
def motor(self, motor: RocketPyMotor):
self._motor = motor
- def get_motor_summary(self) -> MotorSummary:
+ def get_motor_simulation(self) -> MotorSimulation:
"""
- Get the summary of the motor.
+ Get the simulation of the motor.
Returns:
- MotorSummary
+ MotorSimulation
"""
attributes = get_instance_attributes(self.motor)
- motor_summary = MotorSummary(**attributes)
- return motor_summary
+ motor_simulation = MotorSimulation(**attributes)
+ return motor_simulation
def get_motor_binary(self) -> bytes:
"""
diff --git a/lib/services/rocket.py b/src/services/rocket.py
similarity index 91%
rename from lib/services/rocket.py
rename to src/services/rocket.py
index 85db0e8..65bbfe3 100644
--- a/lib/services/rocket.py
+++ b/src/services/rocket.py
@@ -13,11 +13,11 @@
)
from rocketpy.utilities import get_instance_attributes
-from lib import logger
-from lib.models.rocket import Parachute
-from lib.models.aerosurfaces import NoseCone, Tail, Fins
-from lib.services.motor import MotorService
-from lib.views.rocket import RocketView, RocketSummary
+from src import logger
+from src.models.rocket import RocketModel, Parachute
+from src.models.sub.aerosurfaces import NoseCone, Tail, Fins
+from src.services.motor import MotorService
+from src.views.rocket import RocketSimulation
class RocketService:
@@ -27,7 +27,7 @@ def __init__(self, rocket: RocketPyRocket = None):
self._rocket = rocket
@classmethod
- def from_rocket_model(cls, rocket: RocketView) -> Self:
+ def from_rocket_model(cls, rocket: RocketModel) -> Self:
"""
Get the rocketpy rocket object.
@@ -43,7 +43,7 @@ def from_rocket_model(cls, rocket: RocketView) -> Self:
power_off_drag=rocket.power_off_drag,
power_on_drag=rocket.power_on_drag,
center_of_mass_without_motor=rocket.center_of_mass_without_motor,
- coordinate_system_orientation=rocket.coordinate_system_orientation.value.lower(),
+ coordinate_system_orientation=rocket.coordinate_system_orientation,
)
rocketpy_rocket.add_motor(
MotorService.from_motor_model(rocket.motor).motor,
@@ -100,16 +100,16 @@ def rocket(self) -> RocketPyRocket:
def rocket(self, rocket: RocketPyRocket):
self._rocket = rocket
- def get_rocket_summary(self) -> RocketSummary:
+ def get_rocket_simulation(self) -> RocketSimulation:
"""
- Get the summary of the rocket.
+ Get the simulation of the rocket.
Returns:
- RocketSummary
+ RocketSimulation
"""
attributes = get_instance_attributes(self.rocket)
- rocket_summary = RocketSummary(**attributes)
- return rocket_summary
+ rocket_simulation = RocketSimulation(**attributes)
+ return rocket_simulation
def get_rocket_binary(self) -> bytes:
"""
@@ -157,7 +157,7 @@ def get_rocketpy_finset(fins: Fins, kind: str) -> RocketPyFins:
RocketPyEllipticalFins
"""
match kind:
- case "TRAPEZOIDAL":
+ case "trapezoidal":
rocketpy_finset = RocketPyTrapezoidalFins(
n=fins.n,
name=fins.name,
@@ -165,7 +165,7 @@ def get_rocketpy_finset(fins: Fins, kind: str) -> RocketPyFins:
span=fins.span,
**fins.get_additional_parameters(),
)
- case "ELLIPTICAL":
+ case "elliptical":
rocketpy_finset = RocketPyEllipticalFins(
n=fins.n,
name=fins.name,
diff --git a/lib/settings/gunicorn.py b/src/settings/gunicorn.py
similarity index 82%
rename from lib/settings/gunicorn.py
rename to src/settings/gunicorn.py
index a563567..f2b92cd 100644
--- a/lib/settings/gunicorn.py
+++ b/src/settings/gunicorn.py
@@ -1,6 +1,6 @@
import uptrace
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
-from lib.secrets import Secrets
+from src.secrets import Secrets
def post_fork(server, worker): # pylint: disable=unused-argument
@@ -10,7 +10,7 @@ def post_fork(server, worker): # pylint: disable=unused-argument
service_version="2.2.0",
deployment_environment="production",
)
- from lib.api import ( # pylint: disable=import-outside-toplevel
+ from src.api import ( # pylint: disable=import-outside-toplevel
app as fastapi_server,
)
diff --git a/lib/utils.py b/src/utils.py
similarity index 85%
rename from lib/utils.py
rename to src/utils.py
index db3077c..0a8ba45 100644
--- a/lib/utils.py
+++ b/src/utils.py
@@ -1,14 +1,43 @@
# fork of https://github.com/encode/starlette/blob/master/starlette/middleware/gzip.py
import gzip
import io
-import typing
+from typing import Annotated, NoReturn, Any
import numpy as np
+from pydantic import PlainSerializer
from starlette.datastructures import Headers, MutableHeaders
from starlette.types import ASGIApp, Message, Receive, Scope, Send
+def to_python_primitive(v: Any) -> Any:
+ """
+ Convert complex types to Python primitives.
+
+ Args:
+ v: Any value, particularly those with a 'source' attribute
+ containing numpy arrays or generic types.
+
+ Returns:
+ The primitive representation of the input value.
+ """
+ if hasattr(v, "source"):
+ if isinstance(v.source, np.ndarray):
+ return v.source.tolist()
+
+ if isinstance(v.source, (np.generic,)):
+ return v.source.item()
+
+ return str(v.source)
+ return str(v)
+
+
+AnyToPrimitive = Annotated[
+ Any,
+ PlainSerializer(to_python_primitive),
+]
+
+
class RocketPyGZipMiddleware:
def __init__(
self, app: ASGIApp, minimum_size: int = 500, compresslevel: int = 9
@@ -74,13 +103,13 @@ async def send_with_gzip(self, message: Message) -> None:
self.started = True
body = message.get("body", b"")
more_body = message.get("more_body", False)
- if len(body) < (self.minimum_size and not more_body) or any(
+ if ((len(body) < self.minimum_size) and not more_body) or any(
value == b'application/octet-stream'
for header, value in self.initial_message["headers"]
):
# Don't apply GZip to small outgoing responses or octet-streams.
await self.send(self.initial_message)
- await self.send(message)
+ await self.send(message) # pylint: disable=unreachable
elif not more_body:
# Standard GZip response.
self.gzip_file.write(body)
@@ -94,7 +123,7 @@ async def send_with_gzip(self, message: Message) -> None:
message["body"] = body
await self.send(self.initial_message)
- await self.send(message)
+ await self.send(message) # pylint: disable=unreachable
else:
# Initial body in streaming GZip response.
headers = MutableHeaders(raw=self.initial_message["headers"])
@@ -108,7 +137,7 @@ async def send_with_gzip(self, message: Message) -> None:
self.gzip_buffer.truncate()
await self.send(self.initial_message)
- await self.send(message)
+ await self.send(message) # pylint: disable=unreachable
elif message_type == "http.response.body":
# Remaining body in streaming GZip response.
@@ -126,18 +155,5 @@ async def send_with_gzip(self, message: Message) -> None:
await self.send(message)
-async def unattached_send(message: Message) -> typing.NoReturn:
+async def unattached_send(message: Message) -> NoReturn:
raise RuntimeError("send awaitable not set") # pragma: no cover
-
-
-def to_python_primitive(v):
- if hasattr(v, "source"):
- if isinstance(v.source, np.ndarray):
- return v.source.tolist()
-
- if isinstance(v.source, (np.generic,)):
- return v.source.item()
-
- return str(v.source)
-
- return str(v)
diff --git a/src/views/environment.py b/src/views/environment.py
new file mode 100644
index 0000000..4283d52
--- /dev/null
+++ b/src/views/environment.py
@@ -0,0 +1,63 @@
+from typing import Optional
+from datetime import datetime, timedelta
+from src.views.interface import ApiBaseView
+from src.models.environment import EnvironmentModel
+from src.utils import AnyToPrimitive
+
+
+class EnvironmentSimulation(ApiBaseView):
+ message: str = "Environment successfully simulated"
+ latitude: Optional[float] = None
+ longitude: Optional[float] = None
+ elevation: Optional[float] = 1
+ atmospheric_model_type: Optional[str] = None
+ air_gas_constant: Optional[float] = None
+ standard_g: Optional[float] = None
+ earth_radius: Optional[float] = None
+ datum: Optional[str] = None
+ timezone: Optional[str] = None
+ initial_utm_zone: Optional[int] = None
+ initial_utm_letter: Optional[str] = None
+ initial_north: Optional[float] = None
+ initial_east: Optional[float] = None
+ initial_hemisphere: Optional[str] = None
+ initial_ew: Optional[str] = None
+ max_expected_height: Optional[int] = None
+ date: Optional[datetime] = datetime.today() + timedelta(days=1)
+ local_date: Optional[datetime] = datetime.today() + timedelta(days=1)
+ datetime_date: Optional[datetime] = datetime.today() + timedelta(days=1)
+ ellipsoid: Optional[AnyToPrimitive] = None
+ barometric_height: Optional[AnyToPrimitive] = None
+ barometric_height_ISA: Optional[AnyToPrimitive] = None
+ pressure: Optional[AnyToPrimitive] = None
+ pressure_ISA: Optional[AnyToPrimitive] = None
+ temperature: Optional[AnyToPrimitive] = None
+ temperature_ISA: Optional[AnyToPrimitive] = None
+ density: Optional[AnyToPrimitive] = None
+ speed_of_sound: Optional[AnyToPrimitive] = None
+ dynamic_viscosity: Optional[AnyToPrimitive] = None
+ gravity: Optional[AnyToPrimitive] = None
+ somigliana_gravity: Optional[AnyToPrimitive] = None
+ wind_speed: Optional[AnyToPrimitive] = None
+ wind_direction: Optional[AnyToPrimitive] = None
+ wind_heading: Optional[AnyToPrimitive] = None
+ wind_velocity_x: Optional[AnyToPrimitive] = None
+ wind_velocity_y: Optional[AnyToPrimitive] = None
+ calculate_earth_radius: Optional[AnyToPrimitive] = None
+ decimal_degrees_to_arc_seconds: Optional[AnyToPrimitive] = None
+ geodesic_to_utm: Optional[AnyToPrimitive] = None
+ utm_to_geodesic: Optional[AnyToPrimitive] = None
+
+
+class EnvironmentView(EnvironmentModel):
+ environment_id: Optional[str] = None
+
+
+class EnvironmentCreated(ApiBaseView):
+ message: str = "Environment successfully created"
+ environment_id: str
+
+
+class EnvironmentRetrieved(ApiBaseView):
+ message: str = "Environment successfully retrieved"
+ environment: EnvironmentView
diff --git a/src/views/flight.py b/src/views/flight.py
new file mode 100644
index 0000000..1a82f98
--- /dev/null
+++ b/src/views/flight.py
@@ -0,0 +1,168 @@
+from typing import Optional
+from src.models.flight import FlightModel
+from src.views.interface import ApiBaseView
+from src.views.rocket import RocketView, RocketSimulation
+from src.views.environment import EnvironmentSimulation
+from src.utils import AnyToPrimitive
+
+
+class FlightSimulation(RocketSimulation, EnvironmentSimulation):
+ message: str = "Flight successfully simulated"
+ name: Optional[str] = None
+ max_time: Optional[int] = None
+ min_time_step: Optional[int] = None
+ max_time_step: Optional[AnyToPrimitive] = None
+ equations_of_motion: Optional[str] = None
+ heading: Optional[int] = None
+ inclination: Optional[int] = None
+ initial_solution: Optional[list] = None
+ effective_1rl: Optional[float] = None
+ effective_2rl: Optional[float] = None
+ out_of_rail_time: Optional[float] = None
+ out_of_rail_time_index: Optional[int] = None
+ parachute_cd_s: Optional[float] = None
+ rail_length: Optional[float] = None
+ rtol: Optional[float] = None
+ t: Optional[float] = None
+ t_final: Optional[float] = None
+ t_initial: Optional[int] = None
+ terminate_on_apogee: Optional[bool] = None
+ time_overshoot: Optional[bool] = None
+ latitude: Optional[AnyToPrimitive] = None
+ longitude: Optional[AnyToPrimitive] = None
+ M1: Optional[AnyToPrimitive] = None
+ M2: Optional[AnyToPrimitive] = None
+ M3: Optional[AnyToPrimitive] = None
+ R1: Optional[AnyToPrimitive] = None
+ R2: Optional[AnyToPrimitive] = None
+ R3: Optional[AnyToPrimitive] = None
+ acceleration: Optional[AnyToPrimitive] = None
+ aerodynamic_bending_moment: Optional[AnyToPrimitive] = None
+ aerodynamic_drag: Optional[AnyToPrimitive] = None
+ aerodynamic_lift: Optional[AnyToPrimitive] = None
+ aerodynamic_spin_moment: Optional[AnyToPrimitive] = None
+ alpha1: Optional[AnyToPrimitive] = None
+ alpha2: Optional[AnyToPrimitive] = None
+ alpha3: Optional[AnyToPrimitive] = None
+ altitude: Optional[AnyToPrimitive] = None
+ angle_of_attack: Optional[AnyToPrimitive] = None
+ apogee: Optional[AnyToPrimitive] = None
+ apogee_freestream_speed: Optional[AnyToPrimitive] = None
+ apogee_state: Optional[AnyToPrimitive] = None
+ apogee_time: Optional[AnyToPrimitive] = None
+ apogee_x: Optional[AnyToPrimitive] = None
+ apogee_y: Optional[AnyToPrimitive] = None
+ atol: Optional[AnyToPrimitive] = None
+ attitude_angle: Optional[AnyToPrimitive] = None
+ attitude_frequency_response: Optional[AnyToPrimitive] = None
+ attitude_vector_x: Optional[AnyToPrimitive] = None
+ attitude_vector_y: Optional[AnyToPrimitive] = None
+ attitude_vector_z: Optional[AnyToPrimitive] = None
+ ax: Optional[AnyToPrimitive] = None
+ ay: Optional[AnyToPrimitive] = None
+ az: Optional[AnyToPrimitive] = None
+ bearing: Optional[AnyToPrimitive] = None
+ drag_power: Optional[AnyToPrimitive] = None
+ drift: Optional[AnyToPrimitive] = None
+ dynamic_pressure: Optional[AnyToPrimitive] = None
+ e0: Optional[AnyToPrimitive] = None
+ e1: Optional[AnyToPrimitive] = None
+ e2: Optional[AnyToPrimitive] = None
+ e3: Optional[AnyToPrimitive] = None
+ free_stream_speed: Optional[AnyToPrimitive] = None
+ frontal_surface_wind: Optional[AnyToPrimitive] = None
+ function_evaluations: Optional[AnyToPrimitive] = None
+ function_evaluations_per_time_step: Optional[AnyToPrimitive] = None
+ horizontal_speed: Optional[AnyToPrimitive] = None
+ impact_state: Optional[AnyToPrimitive] = None
+ impact_velocity: Optional[AnyToPrimitive] = None
+ initial_stability_margin: Optional[AnyToPrimitive] = None
+ kinetic_energy: Optional[AnyToPrimitive] = None
+ lateral_attitude_angle: Optional[AnyToPrimitive] = None
+ lateral_surface_wind: Optional[AnyToPrimitive] = None
+ mach_number: Optional[AnyToPrimitive] = None
+ max_acceleration: Optional[AnyToPrimitive] = None
+ max_acceleration_power_off: Optional[AnyToPrimitive] = None
+ max_acceleration_power_off_time: Optional[AnyToPrimitive] = None
+ max_acceleration_power_on: Optional[AnyToPrimitive] = None
+ max_acceleration_power_on_time: Optional[AnyToPrimitive] = None
+ max_acceleration_time: Optional[AnyToPrimitive] = None
+ max_dynamic_pressure: Optional[AnyToPrimitive] = None
+ max_dynamic_pressure_time: Optional[AnyToPrimitive] = None
+ max_mach_number: Optional[AnyToPrimitive] = None
+ max_mach_number_time: Optional[AnyToPrimitive] = None
+ max_rail_button1_normal_force: Optional[AnyToPrimitive] = None
+ max_rail_button1_shear_force: Optional[AnyToPrimitive] = None
+ max_rail_button2_normal_force: Optional[AnyToPrimitive] = None
+ max_rail_button2_shear_force: Optional[AnyToPrimitive] = None
+ max_reynolds_number: Optional[AnyToPrimitive] = None
+ max_reynolds_number_time: Optional[AnyToPrimitive] = None
+ max_speed: Optional[AnyToPrimitive] = None
+ max_speed_time: Optional[AnyToPrimitive] = None
+ max_stability_margin: Optional[AnyToPrimitive] = None
+ max_stability_margin_time: Optional[AnyToPrimitive] = None
+ max_total_pressure: Optional[AnyToPrimitive] = None
+ max_total_pressure_time: Optional[AnyToPrimitive] = None
+ min_stability_margin: Optional[AnyToPrimitive] = None
+ min_stability_margin_time: Optional[AnyToPrimitive] = None
+ omega1_frequency_response: Optional[AnyToPrimitive] = None
+ omega2_frequency_response: Optional[AnyToPrimitive] = None
+ omega3_frequency_response: Optional[AnyToPrimitive] = None
+ out_of_rail_stability_margin: Optional[AnyToPrimitive] = None
+ out_of_rail_state: Optional[AnyToPrimitive] = None
+ out_of_rail_velocity: Optional[AnyToPrimitive] = None
+ parachute_events: Optional[AnyToPrimitive] = None
+ path_angle: Optional[AnyToPrimitive] = None
+ phi: Optional[AnyToPrimitive] = None
+ potential_energy: Optional[AnyToPrimitive] = None
+ psi: Optional[AnyToPrimitive] = None
+ rail_button1_normal_force: Optional[AnyToPrimitive] = None
+ rail_button1_shear_force: Optional[AnyToPrimitive] = None
+ rail_button2_normal_force: Optional[AnyToPrimitive] = None
+ rail_button2_shear_force: Optional[AnyToPrimitive] = None
+ reynolds_number: Optional[AnyToPrimitive] = None
+ rotational_energy: Optional[AnyToPrimitive] = None
+ solution: Optional[AnyToPrimitive] = None
+ solution_array: Optional[AnyToPrimitive] = None
+ speed: Optional[AnyToPrimitive] = None
+ stability_margin: Optional[AnyToPrimitive] = None
+ static_margin: Optional[AnyToPrimitive] = None
+ stream_velocity_x: Optional[AnyToPrimitive] = None
+ stream_velocity_y: Optional[AnyToPrimitive] = None
+ stream_velocity_z: Optional[AnyToPrimitive] = None
+ theta: Optional[AnyToPrimitive] = None
+ thrust_power: Optional[AnyToPrimitive] = None
+ time: Optional[AnyToPrimitive] = None
+ time_steps: Optional[AnyToPrimitive] = None
+ total_energy: Optional[AnyToPrimitive] = None
+ total_pressure: Optional[AnyToPrimitive] = None
+ translational_energy: Optional[AnyToPrimitive] = None
+ vx: Optional[AnyToPrimitive] = None
+ vy: Optional[AnyToPrimitive] = None
+ vz: Optional[AnyToPrimitive] = None
+ w1: Optional[AnyToPrimitive] = None
+ w2: Optional[AnyToPrimitive] = None
+ w3: Optional[AnyToPrimitive] = None
+ x: Optional[AnyToPrimitive] = None
+ x_impact: Optional[AnyToPrimitive] = None
+ y: Optional[AnyToPrimitive] = None
+ y_impact: Optional[AnyToPrimitive] = None
+ y_sol: Optional[AnyToPrimitive] = None
+ z: Optional[AnyToPrimitive] = None
+ z_impact: Optional[AnyToPrimitive] = None
+ flight_phases: Optional[AnyToPrimitive] = None
+
+
+class FlightView(FlightModel):
+ flight_id: str
+ rocket: RocketView
+
+
+class FlightCreated(ApiBaseView):
+ message: str = "Flight successfully created"
+ flight_id: str
+
+
+class FlightRetrieved(ApiBaseView):
+ message: str = "Flight successfully retrieved"
+ flight: FlightView
diff --git a/src/views/interface.py b/src/views/interface.py
new file mode 100644
index 0000000..ef58c5a
--- /dev/null
+++ b/src/views/interface.py
@@ -0,0 +1,5 @@
+from pydantic import BaseModel
+
+
+class ApiBaseView(BaseModel):
+ message: str = 'View not implemented'
diff --git a/src/views/motor.py b/src/views/motor.py
new file mode 100644
index 0000000..9f73a17
--- /dev/null
+++ b/src/views/motor.py
@@ -0,0 +1,84 @@
+from typing import List, Optional
+from pydantic import BaseModel
+from src.views.interface import ApiBaseView
+from src.models.motor import MotorModel
+from src.utils import AnyToPrimitive
+
+
+class MotorSimulation(BaseModel):
+ message: str = "Motor successfully simulated"
+ average_thrust: Optional[float] = None
+ burn_duration: Optional[float] = None
+ burn_out_time: Optional[float] = None
+ burn_start_time: Optional[float] = None
+ center_of_dry_mass_position: Optional[float] = None
+ coordinate_system_orientation: str = 'nozzle_to_combustion_chamber'
+ dry_I_11: Optional[float] = None
+ dry_I_12: Optional[float] = None
+ dry_I_13: Optional[float] = None
+ dry_I_22: Optional[float] = None
+ dry_I_23: Optional[float] = None
+ dry_I_33: Optional[float] = None
+ dry_mass: Optional[float] = None
+ grain_burn_out: Optional[float] = None
+ grain_density: Optional[float] = None
+ grain_initial_height: Optional[float] = None
+ grain_initial_inner_radius: Optional[float] = None
+ grain_initial_mass: Optional[float] = None
+ grain_initial_volume: Optional[float] = None
+ grain_number: Optional[int] = None
+ grain_outer_radius: Optional[float] = None
+ grain_separation: Optional[float] = None
+ grains_center_of_mass_position: Optional[float] = None
+ interpolate: Optional[str] = None
+ max_thrust: Optional[float] = None
+ max_thrust_time: Optional[float] = None
+ nozzle_position: Optional[float] = None
+ nozzle_radius: Optional[float] = None
+ propellant_initial_mass: Optional[float] = None
+ throat_area: Optional[float] = None
+ throat_radius: Optional[float] = None
+ thrust_source: Optional[List[List[float]]] = None
+ total_impulse: Optional[float] = None
+ Kn: Optional[AnyToPrimitive] = None
+ I_11: Optional[AnyToPrimitive] = None
+ I_12: Optional[AnyToPrimitive] = None
+ I_13: Optional[AnyToPrimitive] = None
+ I_22: Optional[AnyToPrimitive] = None
+ I_23: Optional[AnyToPrimitive] = None
+ I_33: Optional[AnyToPrimitive] = None
+ burn_area: Optional[AnyToPrimitive] = None
+ burn_rate: Optional[AnyToPrimitive] = None
+ burn_time: Optional[AnyToPrimitive] = None
+ center_of_mass: Optional[AnyToPrimitive] = None
+ center_of_propellant_mass: Optional[AnyToPrimitive] = None
+ exhaust_velocity: Optional[AnyToPrimitive] = None
+ grain_height: Optional[AnyToPrimitive] = None
+ grain_volume: Optional[AnyToPrimitive] = None
+ grain_inner_radius: Optional[AnyToPrimitive] = None
+ mass_flow_rate: Optional[AnyToPrimitive] = None
+ propellant_I_11: Optional[AnyToPrimitive] = None
+ propellant_I_12: Optional[AnyToPrimitive] = None
+ propellant_I_13: Optional[AnyToPrimitive] = None
+ propellant_I_22: Optional[AnyToPrimitive] = None
+ propellant_I_23: Optional[AnyToPrimitive] = None
+ propellant_I_33: Optional[AnyToPrimitive] = None
+ propellant_mass: Optional[AnyToPrimitive] = None
+ reshape_thrust_curve: Optional[AnyToPrimitive] = None
+ total_mass: Optional[AnyToPrimitive] = None
+ total_mass_flow_rate: Optional[AnyToPrimitive] = None
+ thrust: Optional[AnyToPrimitive] = None
+
+
+class MotorView(MotorModel):
+ motor_id: Optional[str] = None
+
+
+class MotorCreated(ApiBaseView):
+ message: str = "Motor successfully created"
+ motor_id: str
+
+
+class MotorRetrieved(ApiBaseView):
+ message: str = "Motor successfully retrieved"
+ motor: MotorView
diff --git a/src/views/rocket.py b/src/views/rocket.py
new file mode 100644
index 0000000..becee4a
--- /dev/null
+++ b/src/views/rocket.py
@@ -0,0 +1,53 @@
+from typing import Optional
+from src.models.rocket import RocketModel
+from src.views.interface import ApiBaseView
+from src.views.motor import MotorView, MotorSimulation
+from src.utils import AnyToPrimitive
+
+
+class RocketSimulation(MotorSimulation):
+ message: str = "Rocket successfully simulated"
+ area: Optional[float] = None
+ coordinate_system_orientation: str = 'tail_to_nose'
+ center_of_mass_without_motor: Optional[float] = None
+ motor_center_of_dry_mass_position: Optional[float] = None
+ motor_position: Optional[float] = None
+ nozzle_position: Optional[float] = None
+ nozzle_to_cdm: Optional[float] = None
+ cp_eccentricity_x: Optional[float] = None
+ cp_eccentricity_y: Optional[float] = None
+ thrust_eccentricity_x: Optional[float] = None
+ thrust_eccentricity_y: Optional[float] = None
+ I_11_without_motor: Optional[AnyToPrimitive] = None
+ I_12_without_motor: Optional[AnyToPrimitive] = None
+ I_13_without_motor: Optional[AnyToPrimitive] = None
+ I_22_without_motor: Optional[AnyToPrimitive] = None
+ I_23_without_motor: Optional[AnyToPrimitive] = None
+ I_33_without_motor: Optional[AnyToPrimitive] = None
+ check_parachute_trigger: Optional[AnyToPrimitive] = None
+ com_to_cdm_function: Optional[AnyToPrimitive] = None
+ cp_position: Optional[AnyToPrimitive] = None
+ motor_center_of_mass_position: Optional[AnyToPrimitive] = None
+ nozzle_gyration_tensor: Optional[AnyToPrimitive] = None
+ power_off_drag: Optional[AnyToPrimitive] = None
+ power_on_drag: Optional[AnyToPrimitive] = None
+ reduced_mass: Optional[AnyToPrimitive] = None
+ stability_margin: Optional[AnyToPrimitive] = None
+ static_margin: Optional[AnyToPrimitive] = None
+ thrust_to_weight: Optional[AnyToPrimitive] = None
+ total_lift_coeff_der: Optional[AnyToPrimitive] = None
+
+
+class RocketView(RocketModel):
+ rocket_id: Optional[str] = None
+ motor: MotorView
+
+
+class RocketCreated(ApiBaseView):
+ message: str = "Rocket successfully created"
+ rocket_id: str
+
+
+class RocketRetrieved(ApiBaseView):
+ message: str = "Rocket successfully retrieved"
+ rocket: RocketView
diff --git a/tests/test_routes/test_environments_route.py b/tests/test_routes/test_environments_route.py
deleted file mode 100644
index ceea53a..0000000
--- a/tests/test_routes/test_environments_route.py
+++ /dev/null
@@ -1,259 +0,0 @@
-from unittest.mock import patch
-import json
-import pytest
-from fastapi.testclient import TestClient
-from fastapi import HTTPException, status
-from lib.models.environment import Env
-from lib.controllers.environment import EnvController
-from lib.views.environment import (
- EnvCreated,
- EnvUpdated,
- EnvDeleted,
- EnvSummary,
-)
-from lib import app
-
-client = TestClient(app)
-
-
-@pytest.fixture
-def stub_env_summary():
- env_summary = EnvSummary()
- env_summary_json = env_summary.model_dump_json()
- return json.loads(env_summary_json)
-
-
-def test_create_env(stub_env):
- with patch.object(
- EnvController, 'create_env', return_value=EnvCreated(env_id='123')
- ) as mock_create_env:
- response = client.post('/environments/', json=stub_env)
- assert response.status_code == 200
- assert response.json() == {
- 'env_id': '123',
- 'message': 'Environment successfully created',
- }
- mock_create_env.assert_called_once_with(Env(**stub_env))
-
-
-def test_create_env_optional_params():
- test_object = {
- 'latitude': 0,
- 'longitude': 0,
- 'elevation': 1,
- 'atmospheric_model_type': 'STANDARD_ATMOSPHERE',
- 'atmospheric_model_file': None,
- 'date': '2021-01-01T00:00:00',
- }
- with patch.object(
- EnvController, 'create_env', return_value=EnvCreated(env_id='123')
- ) as mock_create_env:
- response = client.post('/environments/', json=test_object)
- assert response.status_code == 200
- assert response.json() == {
- 'env_id': '123',
- 'message': 'Environment successfully created',
- }
- mock_create_env.assert_called_once_with(Env(**test_object))
-
-
-def test_create_env_invalid_input():
- response = client.post(
- '/environments/', json={'latitude': 'foo', 'longitude': 'bar'}
- )
- assert response.status_code == 422
-
-
-def test_create_env_server_error(stub_env):
- with patch.object(
- EnvController,
- 'create_env',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.post('/environments/', json=stub_env)
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_read_env(stub_env):
- with patch.object(
- EnvController, 'get_env_by_id', return_value=Env(**stub_env)
- ) as mock_read_env:
- response = client.get('/environments/123')
- assert response.status_code == 200
- assert response.json() == stub_env
- mock_read_env.assert_called_once_with('123')
-
-
-def test_read_env_not_found():
- with patch.object(
- EnvController,
- 'get_env_by_id',
- side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND),
- ) as mock_read_env:
- response = client.get('/environments/123')
- assert response.status_code == 404
- assert response.json() == {'detail': 'Not Found'}
- mock_read_env.assert_called_once_with('123')
-
-
-def test_read_env_server_error():
- with patch.object(
- EnvController,
- 'get_env_by_id',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.get('/environments/123')
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_update_env(stub_env):
- with patch.object(
- EnvController,
- 'update_env_by_id',
- return_value=EnvUpdated(env_id='123'),
- ) as mock_update_env:
- response = client.put('/environments/123', json=stub_env)
- assert response.status_code == 200
- assert response.json() == {
- 'env_id': '123',
- 'message': 'Environment successfully updated',
- }
- mock_update_env.assert_called_once_with('123', Env(**stub_env))
-
-
-def test_update_env_invalid_input():
- response = client.put(
- '/environments/123', json={'latitude': 'foo', 'longitude': 'bar'}
- )
- assert response.status_code == 422
-
-
-def test_update_env_not_found(stub_env):
- with patch.object(
- EnvController,
- 'update_env_by_id',
- side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND),
- ):
- response = client.put('/environments/123', json=stub_env)
- assert response.status_code == 404
- assert response.json() == {'detail': 'Not Found'}
-
-
-def test_update_env_server_error(stub_env):
- with patch.object(
- EnvController,
- 'update_env_by_id',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.put('/environments/123', json=stub_env)
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_delete_env():
- with patch.object(
- EnvController,
- 'delete_env_by_id',
- return_value=EnvDeleted(env_id='123'),
- ) as mock_delete_env:
- response = client.delete('/environments/123')
- assert response.status_code == 200
- assert response.json() == {
- 'env_id': '123',
- 'message': 'Environment successfully deleted',
- }
- mock_delete_env.assert_called_once_with('123')
-
-
-def test_delete_env_server_error():
- with patch.object(
- EnvController,
- 'delete_env_by_id',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.delete('/environments/123')
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_simulate_env(stub_env_summary):
- with patch.object(
- EnvController,
- 'simulate_env',
- return_value=EnvSummary(**stub_env_summary),
- ) as mock_simulate_env:
- response = client.get('/environments/123/summary')
- assert response.status_code == 200
- assert response.json() == stub_env_summary
- mock_simulate_env.assert_called_once_with('123')
-
-
-def test_simulate_env_not_found():
- with patch.object(
- EnvController,
- 'simulate_env',
- side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND),
- ) as mock_simulate_env:
- response = client.get('/environments/123/summary')
- assert response.status_code == 404
- assert response.json() == {'detail': 'Not Found'}
- mock_simulate_env.assert_called_once_with('123')
-
-
-def test_simulate_env_server_error():
- with patch.object(
- EnvController,
- 'simulate_env',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.get('/environments/123/summary')
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_read_rocketpy_env():
- with patch.object(
- EnvController, 'get_rocketpy_env_binary', return_value=b'rocketpy'
- ) as mock_read_rocketpy_env:
- response = client.get('/environments/123/rocketpy')
- assert response.status_code == 203
- assert response.content == b'rocketpy'
- assert response.headers['content-type'] == 'application/octet-stream'
- mock_read_rocketpy_env.assert_called_once_with('123')
-
-
-def test_read_rocketpy_env_not_found():
- with patch.object(
- EnvController,
- 'get_rocketpy_env_binary',
- side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND),
- ) as mock_read_rocketpy_env:
- response = client.get('/environments/123/rocketpy')
- assert response.status_code == 404
- assert response.json() == {'detail': 'Not Found'}
- mock_read_rocketpy_env.assert_called_once_with('123')
-
-
-def test_read_rocketpy_env_server_error():
- with patch.object(
- EnvController,
- 'get_rocketpy_env_binary',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.get('/environments/123/rocketpy')
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
diff --git a/tests/test_routes/test_flights_route.py b/tests/test_routes/test_flights_route.py
deleted file mode 100644
index 695fb95..0000000
--- a/tests/test_routes/test_flights_route.py
+++ /dev/null
@@ -1,372 +0,0 @@
-from unittest.mock import patch
-import json
-import pytest
-from fastapi.testclient import TestClient
-from fastapi import HTTPException, status
-from lib.models.environment import Env
-from lib.models.flight import Flight
-from lib.models.motor import Motor, MotorKinds
-from lib.models.rocket import Rocket
-from lib.controllers.flight import FlightController
-from lib.views.motor import MotorView
-from lib.views.rocket import RocketView
-from lib.views.flight import (
- FlightCreated,
- FlightUpdated,
- FlightDeleted,
- FlightSummary,
- FlightView,
-)
-from lib import app
-
-client = TestClient(app)
-
-
-@pytest.fixture
-def stub_flight(stub_env, stub_rocket):
- flight = {
- 'name': 'Test Flight',
- 'environment': stub_env,
- 'rocket': stub_rocket,
- 'rail_length': 1,
- 'time_overshoot': True,
- 'terminate_on_apogee': True,
- 'equations_of_motion': 'STANDARD',
- }
- return flight
-
-
-@pytest.fixture
-def stub_flight_summary():
- flight_summary = FlightSummary()
- flight_summary_json = flight_summary.model_dump_json()
- return json.loads(flight_summary_json)
-
-
-def test_create_flight(stub_flight):
- with patch.object(
- FlightController,
- 'create_flight',
- return_value=FlightCreated(flight_id='123'),
- ) as mock_create_flight:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/flights/', json=stub_flight, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'flight_id': '123',
- 'message': 'Flight successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID)
- mock_create_flight.assert_called_once_with(Flight(**stub_flight))
-
-
-def test_create_flight_optional_params(stub_flight):
- stub_flight.update(
- {
- 'inclination': 0,
- 'heading': 0,
- 'max_time': 1,
- 'max_time_step': 1.0,
- 'min_time_step': 1,
- 'rtol': 1.0,
- 'atol': 1.0,
- 'verbose': True,
- }
- )
- with patch.object(
- FlightController,
- 'create_flight',
- return_value=FlightCreated(flight_id='123'),
- ) as mock_create_flight:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/flights/', json=stub_flight, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'flight_id': '123',
- 'message': 'Flight successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID)
- mock_create_flight.assert_called_once_with(Flight(**stub_flight))
-
-
-def test_create_flight_invalid_input():
- response = client.post(
- '/flights/', json={'environment': 'foo', 'rocket': 'bar'}
- )
- assert response.status_code == 422
-
-
-def test_create_flight_server_error(stub_flight):
- with patch.object(
- FlightController,
- 'create_flight',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.post(
- '/flights/', json=stub_flight, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_read_flight(stub_flight, stub_rocket, stub_motor):
- del stub_rocket['motor']
- del stub_flight['rocket']
- motor_view = MotorView(**stub_motor, selected_motor_kind=MotorKinds.HYBRID)
- rocket_view = RocketView(**stub_rocket, motor=motor_view)
- flight_view = FlightView(**stub_flight, rocket=rocket_view)
- with patch.object(
- FlightController,
- 'get_flight_by_id',
- return_value=flight_view,
- ) as mock_read_flight:
- response = client.get('/flights/123')
- assert response.status_code == 200
- assert response.json() == json.loads(flight_view.model_dump_json())
- mock_read_flight.assert_called_once_with('123')
-
-
-def test_read_flight_not_found():
- with patch.object(
- FlightController,
- 'get_flight_by_id',
- side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND),
- ) as mock_read_flight:
- response = client.get('/flights/123')
- assert response.status_code == 404
- assert response.json() == {'detail': 'Not Found'}
- mock_read_flight.assert_called_once_with('123')
-
-
-def test_read_flight_server_error():
- with patch.object(
- FlightController,
- 'get_flight_by_id',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.get('/flights/123')
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_update_flight(stub_flight):
- with patch.object(
- FlightController,
- 'update_flight_by_id',
- return_value=FlightUpdated(flight_id='123'),
- ) as mock_update_flight:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.put(
- '/flights/123',
- json=stub_flight,
- params={'motor_kind': 'GENERIC'},
- )
- assert response.status_code == 200
- assert response.json() == {
- 'flight_id': '123',
- 'message': 'Flight successfully updated',
- }
- mock_update_flight.assert_called_once_with(
- '123', Flight(**stub_flight)
- )
- mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC)
-
-
-def test_update_env_by_flight_id(stub_env):
- with patch.object(
- FlightController,
- 'update_env_by_flight_id',
- return_value=FlightUpdated(flight_id='123'),
- ) as mock_update_flight:
- response = client.put('/flights/123/env', json=stub_env)
- assert response.status_code == 200
- assert response.json() == {
- 'flight_id': '123',
- 'message': 'Flight successfully updated',
- }
- mock_update_flight.assert_called_once_with('123', env=Env(**stub_env))
-
-
-def test_update_rocket_by_flight_id(stub_rocket):
- with patch.object(
- FlightController,
- 'update_rocket_by_flight_id',
- return_value=FlightUpdated(flight_id='123'),
- ) as mock_update_flight:
- response = client.put(
- '/flights/123/rocket',
- json=stub_rocket,
- params={'motor_kind': 'GENERIC'},
- )
- assert response.status_code == 200
- assert response.json() == {
- 'flight_id': '123',
- 'message': 'Flight successfully updated',
- }
- assert mock_update_flight.call_count == 1
- assert mock_update_flight.call_args[0][0] == '123'
- assert mock_update_flight.call_args[1]['rocket'].model_dump() == Rocket(**stub_rocket).model_dump()
-
-
-def test_update_env_by_flight_id_invalid_input():
- response = client.put('/flights/123', json={'environment': 'foo'})
- assert response.status_code == 422
-
-
-def test_update_rocket_by_flight_id_invalid_input():
- response = client.put('/flights/123', json={'rocket': 'bar'})
- assert response.status_code == 422
-
-
-def test_update_flight_invalid_input():
- response = client.put(
- '/flights/123',
- json={'environment': 'foo', 'rocket': 'bar'},
- params={'motor_kind': 'GENERIC'},
- )
- assert response.status_code == 422
-
-
-def test_update_flight_not_found(stub_flight):
- with patch.object(
- FlightController,
- 'update_flight_by_id',
- side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND),
- ):
- response = client.put(
- '/flights/123', json=stub_flight, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 404
- assert response.json() == {'detail': 'Not Found'}
-
-
-def test_update_flight_server_error(stub_flight):
- with patch.object(
- FlightController,
- 'update_flight_by_id',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.put(
- '/flights/123', json=stub_flight, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_delete_flight():
- with patch.object(
- FlightController,
- 'delete_flight_by_id',
- return_value=FlightDeleted(flight_id='123'),
- ) as mock_delete_flight:
- response = client.delete('/flights/123')
- assert response.status_code == 200
- assert response.json() == {
- 'flight_id': '123',
- 'message': 'Flight successfully deleted',
- }
- mock_delete_flight.assert_called_once_with('123')
-
-
-def test_delete_flight_server_error():
- with patch.object(
- FlightController,
- 'delete_flight_by_id',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.delete('/flights/123')
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_simulate_flight(stub_flight_summary):
- with patch.object(
- FlightController,
- 'simulate_flight',
- return_value=FlightSummary(**stub_flight_summary),
- ) as mock_simulate_flight:
- response = client.get('/flights/123/summary')
- assert response.status_code == 200
- assert response.json() == stub_flight_summary
- mock_simulate_flight.assert_called_once_with('123')
-
-
-def test_simulate_flight_not_found():
- with patch.object(
- FlightController,
- 'simulate_flight',
- side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND),
- ) as mock_simulate_flight:
- response = client.get('/flights/123/summary')
- assert response.status_code == 404
- assert response.json() == {'detail': 'Not Found'}
- mock_simulate_flight.assert_called_once_with('123')
-
-
-def test_simulate_flight_server_error():
- with patch.object(
- FlightController,
- 'simulate_flight',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.get('/flights/123/summary')
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_read_rocketpy_flight():
- with patch.object(
- FlightController,
- 'get_rocketpy_flight_binary',
- return_value=b'rocketpy',
- ) as mock_read_rocketpy_flight:
- response = client.get('/flights/123/rocketpy')
- assert response.status_code == 203
- assert response.content == b'rocketpy'
- assert response.headers['content-type'] == 'application/octet-stream'
- mock_read_rocketpy_flight.assert_called_once_with('123')
-
-
-def test_read_rocketpy_flight_not_found():
- with patch.object(
- FlightController,
- 'get_rocketpy_flight_binary',
- side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND),
- ) as mock_read_rocketpy_flight:
- response = client.get('/flights/123/rocketpy')
- assert response.status_code == 404
- assert response.json() == {'detail': 'Not Found'}
- mock_read_rocketpy_flight.assert_called_once_with('123')
-
-
-def test_read_rocketpy_flight_server_error():
- with patch.object(
- FlightController,
- 'get_rocketpy_flight_binary',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.get('/flights/123/rocketpy')
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
diff --git a/tests/test_routes/test_motors_route.py b/tests/test_routes/test_motors_route.py
deleted file mode 100644
index 9f32a7b..0000000
--- a/tests/test_routes/test_motors_route.py
+++ /dev/null
@@ -1,481 +0,0 @@
-from unittest.mock import patch
-import json
-import pytest
-from fastapi.testclient import TestClient
-from fastapi import HTTPException, status
-from lib.models.motor import (
- Motor,
- MotorKinds,
-)
-from lib.controllers.motor import MotorController
-from lib.views.motor import (
- MotorCreated,
- MotorUpdated,
- MotorDeleted,
- MotorSummary,
- MotorView,
-)
-from lib import app
-
-client = TestClient(app)
-
-
-@pytest.fixture
-def stub_motor_summary():
- motor_summary = MotorSummary()
- motor_summary_json = motor_summary.model_dump_json()
- return json.loads(motor_summary_json)
-
-
-def test_create_motor(stub_motor):
- with patch.object(
- MotorController,
- 'create_motor',
- return_value=MotorCreated(motor_id='123'),
- ) as mock_create_motor:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/motors/', json=stub_motor, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'motor_id': '123',
- 'message': 'Motor successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID)
- mock_create_motor.assert_called_once_with(Motor(**stub_motor))
-
-
-def test_create_motor_optional_params(stub_motor):
- stub_motor.update(
- {
- 'interpolation_method': 'LINEAR',
- 'coordinate_system_orientation': 'NOZZLE_TO_COMBUSTION_CHAMBER',
- 'reshape_thrust_curve': False,
- }
- )
- with patch.object(
- MotorController,
- 'create_motor',
- return_value=MotorCreated(motor_id='123'),
- ) as mock_create_motor:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/motors/', json=stub_motor, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'motor_id': '123',
- 'message': 'Motor successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID)
- mock_create_motor.assert_called_once_with(Motor(**stub_motor))
-
-
-def test_create_generic_motor(stub_motor):
- stub_motor.update(
- {
- 'chamber_radius': 0,
- 'chamber_height': 0,
- 'chamber_position': 0,
- 'propellant_initial_mass': 0,
- 'nozzle_position': 0,
- }
- )
- with patch.object(
- MotorController,
- 'create_motor',
- return_value=MotorCreated(motor_id='123'),
- ) as mock_create_motor:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/motors/', json=stub_motor, params={'motor_kind': 'GENERIC'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'motor_id': '123',
- 'message': 'Motor successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC)
- mock_create_motor.assert_called_once_with(Motor(**stub_motor))
-
-
-def test_create_liquid_motor_level_tank(stub_motor, stub_level_tank):
- stub_motor.update({'tanks': [stub_level_tank]})
- with patch.object(
- MotorController,
- 'create_motor',
- return_value=MotorCreated(motor_id='123'),
- ) as mock_create_motor:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/motors/', json=stub_motor, params={'motor_kind': 'LIQUID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'motor_id': '123',
- 'message': 'Motor successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID)
- mock_create_motor.assert_called_once_with(Motor(**stub_motor))
-
-
-def test_create_liquid_motor_mass_flow_tank(stub_motor, stub_mass_flow_tank):
- stub_motor.update({'tanks': [stub_mass_flow_tank]})
- with patch.object(
- MotorController,
- 'create_motor',
- return_value=MotorCreated(motor_id='123'),
- ) as mock_create_motor:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/motors/', json=stub_motor, params={'motor_kind': 'LIQUID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'motor_id': '123',
- 'message': 'Motor successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID)
- mock_create_motor.assert_called_once_with(Motor(**stub_motor))
-
-
-def test_create_liquid_motor_ullage_tank(stub_motor, stub_ullage_tank):
- stub_motor.update({'tanks': [stub_ullage_tank]})
- with patch.object(
- MotorController,
- 'create_motor',
- return_value=MotorCreated(motor_id='123'),
- ) as mock_create_motor:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/motors/', json=stub_motor, params={'motor_kind': 'LIQUID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'motor_id': '123',
- 'message': 'Motor successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID)
- mock_create_motor.assert_called_once_with(Motor(**stub_motor))
-
-
-def test_create_liquid_motor_mass_tank(stub_motor, stub_mass_tank):
- stub_motor.update({'tanks': [stub_mass_tank]})
- with patch.object(
- MotorController,
- 'create_motor',
- return_value=MotorCreated(motor_id='123'),
- ) as mock_create_motor:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/motors/', json=stub_motor, params={'motor_kind': 'LIQUID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'motor_id': '123',
- 'message': 'Motor successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID)
- mock_create_motor.assert_called_once_with(Motor(**stub_motor))
-
-
-def test_create_hybrid_motor(stub_motor, stub_level_tank):
- stub_motor.update(
- {
- 'grain_number': 0,
- 'grain_density': 0,
- 'grain_outer_radius': 0,
- 'grain_initial_inner_radius': 0,
- 'grain_initial_height': 0,
- 'grains_center_of_mass_position': 0,
- 'grain_separation': 0,
- 'throat_radius': 0,
- 'tanks': [stub_level_tank],
- }
- )
- with patch.object(
- MotorController,
- 'create_motor',
- return_value=MotorCreated(motor_id='123'),
- ) as mock_create_motor:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/motors/', json=stub_motor, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'motor_id': '123',
- 'message': 'Motor successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID)
- mock_create_motor.assert_called_once_with(Motor(**stub_motor))
-
-
-def test_create_solid_motor(stub_motor):
- stub_motor.update(
- {
- 'grain_number': 0,
- 'grain_density': 0,
- 'grain_outer_radius': 0,
- 'grain_initial_inner_radius': 0,
- 'grain_initial_height': 0,
- 'grains_center_of_mass_position': 0,
- 'grain_separation': 0,
- }
- )
- with patch.object(
- MotorController,
- 'create_motor',
- return_value=MotorCreated(motor_id='123'),
- ) as mock_create_motor:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/motors/', json=stub_motor, params={'motor_kind': 'SOLID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'motor_id': '123',
- 'message': 'Motor successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.SOLID)
- mock_create_motor.assert_called_once_with(Motor(**stub_motor))
-
-
-def test_create_motor_invalid_input():
- response = client.post(
- '/motors/', json={'burn_time': 'foo', 'nozzle_radius': 'bar'}
- )
- assert response.status_code == 422
-
-
-def test_create_motor_server_error(stub_motor):
- with patch.object(
- MotorController,
- 'create_motor',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.post(
- '/motors/', json=stub_motor, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_read_motor(stub_motor):
- stub_motor.update({'selected_motor_kind': 'HYBRID'})
- with patch.object(
- MotorController,
- 'get_motor_by_id',
- return_value=MotorView(**stub_motor),
- ) as mock_read_motor:
- response = client.get('/motors/123')
- assert response.status_code == 200
- assert response.json() == stub_motor
- mock_read_motor.assert_called_once_with('123')
-
-
-def test_read_motor_not_found():
- with patch.object(
- MotorController,
- 'get_motor_by_id',
- side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND),
- ) as mock_read_motor:
- response = client.get('/motors/123')
- assert response.status_code == 404
- assert response.json() == {'detail': 'Not Found'}
- mock_read_motor.assert_called_once_with('123')
-
-
-def test_read_motor_server_error():
- with patch.object(
- MotorController,
- 'get_motor_by_id',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.get('/motors/123')
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_update_motor(stub_motor):
- with patch.object(
- MotorController,
- 'update_motor_by_id',
- return_value=MotorUpdated(motor_id='123'),
- ) as mock_update_motor:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.put(
- '/motors/123', json=stub_motor, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'motor_id': '123',
- 'message': 'Motor successfully updated',
- }
- mock_update_motor.assert_called_once_with(
- '123', Motor(**stub_motor)
- )
- mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID)
-
-
-def test_update_motor_invalid_input():
- response = client.put(
- '/motors/123',
- json={'burn_time': 'foo', 'nozzle_radius': 'bar'},
- params={'motor_kind': 'HYBRID'},
- )
- assert response.status_code == 422
-
-
-def test_update_motor_not_found(stub_motor):
- with patch.object(
- MotorController,
- 'update_motor_by_id',
- side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND),
- ):
- response = client.put(
- '/motors/123', json=stub_motor, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 404
- assert response.json() == {'detail': 'Not Found'}
-
-
-def test_update_motor_server_error(stub_motor):
- with patch.object(
- MotorController,
- 'update_motor_by_id',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.put(
- '/motors/123', json=stub_motor, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_delete_motor():
- with patch.object(
- MotorController,
- 'delete_motor_by_id',
- return_value=MotorDeleted(motor_id='123'),
- ) as mock_delete_motor:
- response = client.delete('/motors/123')
- assert response.status_code == 200
- assert response.json() == {
- 'motor_id': '123',
- 'message': 'Motor successfully deleted',
- }
- mock_delete_motor.assert_called_once_with('123')
-
-
-def test_delete_motor_server_error():
- with patch.object(
- MotorController,
- 'delete_motor_by_id',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.delete('/motors/123')
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_simulate_motor(stub_motor_summary):
- with patch.object(
- MotorController,
- 'simulate_motor',
- return_value=MotorSummary(**stub_motor_summary),
- ) as mock_simulate_motor:
- response = client.get('/motors/123/summary')
- assert response.status_code == 200
- assert response.json() == stub_motor_summary
- mock_simulate_motor.assert_called_once_with('123')
-
-
-def test_simulate_motor_not_found():
- with patch.object(
- MotorController,
- 'simulate_motor',
- side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND),
- ) as mock_simulate_motor:
- response = client.get('/motors/123/summary')
- assert response.status_code == 404
- assert response.json() == {'detail': 'Not Found'}
- mock_simulate_motor.assert_called_once_with('123')
-
-
-def test_simulate_motor_server_error():
- with patch.object(
- MotorController,
- 'simulate_motor',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.get('/motors/123/summary')
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_read_rocketpy_motor():
- with patch.object(
- MotorController, 'get_rocketpy_motor_binary', return_value=b'rocketpy'
- ) as mock_read_rocketpy_motor:
- response = client.get('/motors/123/rocketpy')
- assert response.status_code == 203
- assert response.content == b'rocketpy'
- assert response.headers['content-type'] == 'application/octet-stream'
- mock_read_rocketpy_motor.assert_called_once_with('123')
-
-
-def test_read_rocketpy_motor_not_found():
- with patch.object(
- MotorController,
- 'get_rocketpy_motor_binary',
- side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND),
- ) as mock_read_rocketpy_motor:
- response = client.get('/motors/123/rocketpy')
- assert response.status_code == 404
- assert response.json() == {'detail': 'Not Found'}
- mock_read_rocketpy_motor.assert_called_once_with('123')
-
-
-def test_read_rocketpy_motor_server_error():
- with patch.object(
- MotorController,
- 'get_rocketpy_motor_binary',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.get('/motors/123/rocketpy')
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
diff --git a/tests/test_routes/test_rockets_route.py b/tests/test_routes/test_rockets_route.py
deleted file mode 100644
index f689b98..0000000
--- a/tests/test_routes/test_rockets_route.py
+++ /dev/null
@@ -1,551 +0,0 @@
-from unittest.mock import patch
-import json
-import pytest
-from fastapi.testclient import TestClient
-from fastapi import HTTPException, status
-from lib.models.aerosurfaces import (
- Tail,
- RailButtons,
- Parachute,
-)
-from lib.models.rocket import Rocket
-from lib.models.motor import (
- Motor,
- MotorKinds,
-)
-from lib.controllers.rocket import RocketController
-from lib.views.motor import MotorView
-from lib.views.rocket import (
- RocketCreated,
- RocketUpdated,
- RocketDeleted,
- RocketSummary,
- RocketView,
-)
-from lib import app
-
-client = TestClient(app)
-
-
-@pytest.fixture
-def stub_rocket_summary():
- rocket_summary = RocketSummary()
- rocket_summary_json = rocket_summary.model_dump_json()
- return json.loads(rocket_summary_json)
-
-
-@pytest.fixture
-def stub_tail():
- tail = Tail(
- name='tail',
- top_radius=0,
- bottom_radius=0,
- length=0,
- position=0,
- radius=0,
- )
- tail_json = tail.model_dump_json()
- return json.loads(tail_json)
-
-
-@pytest.fixture
-def stub_rail_buttons():
- rail_buttons = RailButtons(
- upper_button_position=0,
- lower_button_position=0,
- angular_position=0,
- )
- rail_buttons_json = rail_buttons.model_dump_json()
- return json.loads(rail_buttons_json)
-
-
-@pytest.fixture
-def stub_parachute():
- parachute = Parachute(
- name='parachute',
- cd_s=0,
- sampling_rate=1,
- lag=0,
- trigger='trigger',
- noise=(0, 0, 0),
- )
- parachute_json = parachute.model_dump_json()
- return json.loads(parachute_json)
-
-
-def test_create_rocket(stub_rocket):
- with patch.object(
- RocketController,
- 'create_rocket',
- return_value=RocketCreated(rocket_id='123'),
- ) as mock_create_rocket:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/rockets/', json=stub_rocket, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'rocket_id': '123',
- 'message': 'Rocket successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID)
- mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket))
-
-
-def test_create_rocket_optional_params(
- stub_rocket,
- stub_tail,
- stub_rail_buttons,
- stub_parachute,
-):
- stub_rocket.update(
- {
- 'parachutes': [stub_parachute],
- 'rail_buttons': stub_rail_buttons,
- 'tail': stub_tail,
- }
- )
- with patch.object(
- RocketController,
- 'create_rocket',
- return_value=RocketCreated(rocket_id='123'),
- ) as mock_create_rocket:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/rockets/', json=stub_rocket, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'rocket_id': '123',
- 'message': 'Rocket successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID)
- mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket))
-
-
-def test_create_generic_motor_rocket(stub_rocket, stub_motor):
- stub_motor.update(
- {
- 'chamber_radius': 0,
- 'chamber_height': 0,
- 'chamber_position': 0,
- 'propellant_initial_mass': 0,
- 'nozzle_position': 0,
- }
- )
- stub_rocket.update({'motor': stub_motor})
- with patch.object(
- RocketController,
- 'create_rocket',
- return_value=RocketCreated(rocket_id='123'),
- ) as mock_create_rocket:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/rockets/', json=stub_rocket, params={'motor_kind': 'GENERIC'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'rocket_id': '123',
- 'message': 'Rocket successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC)
- mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket))
-
-
-def test_create_liquid_motor_level_tank_rocket(
- stub_rocket, stub_motor, stub_level_tank
-):
- stub_motor.update({'tanks': [stub_level_tank]})
- stub_rocket.update({'motor': stub_motor})
- with patch.object(
- RocketController,
- 'create_rocket',
- return_value=RocketCreated(rocket_id='123'),
- ) as mock_create_rocket:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/rockets/', json=stub_rocket, params={'motor_kind': 'LIQUID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'rocket_id': '123',
- 'message': 'Rocket successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID)
- mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket))
-
-
-def test_create_liquid_motor_mass_flow_tank_rocket(
- stub_rocket, stub_motor, stub_mass_flow_tank
-):
- stub_motor.update({'tanks': [stub_mass_flow_tank]})
- stub_rocket.update({'motor': stub_motor})
- with patch.object(
- RocketController,
- 'create_rocket',
- return_value=RocketCreated(rocket_id='123'),
- ) as mock_create_rocket:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/rockets/', json=stub_rocket, params={'motor_kind': 'LIQUID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'rocket_id': '123',
- 'message': 'Rocket successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID)
- mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket))
-
-
-def test_create_liquid_motor_ullage_tank_rocket(
- stub_rocket, stub_motor, stub_ullage_tank
-):
- stub_motor.update({'tanks': [stub_ullage_tank]})
- stub_rocket.update({'motor': stub_motor})
- with patch.object(
- RocketController,
- 'create_rocket',
- return_value=RocketCreated(rocket_id='123'),
- ) as mock_create_rocket:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/rockets/', json=stub_rocket, params={'motor_kind': 'LIQUID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'rocket_id': '123',
- 'message': 'Rocket successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID)
- mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket))
-
-
-def test_create_liquid_motor_mass_tank_rocket(
- stub_rocket, stub_motor, stub_mass_tank
-):
- stub_motor.update({'tanks': [stub_mass_tank]})
- stub_rocket.update({'motor': stub_motor})
- with patch.object(
- RocketController,
- 'create_rocket',
- return_value=RocketCreated(rocket_id='123'),
- ) as mock_create_rocket:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/rockets/', json=stub_rocket, params={'motor_kind': 'LIQUID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'rocket_id': '123',
- 'message': 'Rocket successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.LIQUID)
- mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket))
-
-
-def test_create_hybrid_motor_rocket(stub_rocket, stub_motor, stub_level_tank):
- stub_motor.update(
- {
- 'grain_number': 0,
- 'grain_density': 0,
- 'grain_outer_radius': 0,
- 'grain_initial_inner_radius': 0,
- 'grain_initial_height': 0,
- 'grains_center_of_mass_position': 0,
- 'grain_separation': 0,
- 'throat_radius': 0,
- 'tanks': [stub_level_tank],
- }
- )
- stub_rocket.update({'motor': stub_motor})
- with patch.object(
- RocketController,
- 'create_rocket',
- return_value=RocketCreated(rocket_id='123'),
- ) as mock_create_rocket:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/rockets/', json=stub_rocket, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'rocket_id': '123',
- 'message': 'Rocket successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.HYBRID)
- mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket))
-
-
-def test_create_solid_motor_rocket(stub_rocket, stub_motor):
- stub_motor.update(
- {
- 'grain_number': 0,
- 'grain_density': 0,
- 'grain_outer_radius': 0,
- 'grain_initial_inner_radius': 0,
- 'grain_initial_height': 0,
- 'grains_center_of_mass_position': 0,
- 'grain_separation': 0,
- }
- )
- stub_rocket.update({'motor': stub_motor})
- with patch.object(
- RocketController,
- 'create_rocket',
- return_value=RocketCreated(rocket_id='123'),
- ) as mock_create_rocket:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.post(
- '/rockets/', json=stub_rocket, params={'motor_kind': 'SOLID'}
- )
- assert response.status_code == 200
- assert response.json() == {
- 'rocket_id': '123',
- 'message': 'Rocket successfully created',
- }
- mock_set_motor_kind.assert_called_once_with(MotorKinds.SOLID)
- mock_create_rocket.assert_called_once_with(Rocket(**stub_rocket))
-
-
-def test_create_rocket_invalid_input():
- response = client.post('/rockets/', json={'radius': 'foo', 'mass': 'bar'})
- assert response.status_code == 422
-
-
-def test_create_rocket_server_error(stub_rocket):
- with patch.object(
- RocketController,
- 'create_rocket',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.post(
- '/rockets/', json=stub_rocket, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_read_rocket(stub_rocket, stub_motor):
- del stub_rocket['motor']
- motor_view = MotorView(**stub_motor, selected_motor_kind=MotorKinds.HYBRID)
- rocket_view = RocketView(**stub_rocket, motor=motor_view)
- with patch.object(
- RocketController,
- 'get_rocket_by_id',
- return_value=rocket_view,
- ) as mock_read_rocket:
- response = client.get('/rockets/123')
- assert response.status_code == 200
- assert response.json() == json.loads(rocket_view.model_dump_json())
- mock_read_rocket.assert_called_once_with('123')
-
-
-def test_read_rocket_not_found():
- with patch.object(
- RocketController,
- 'get_rocket_by_id',
- side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND),
- ) as mock_read_rocket:
- response = client.get('/rockets/123')
- assert response.status_code == 404
- assert response.json() == {'detail': 'Not Found'}
- mock_read_rocket.assert_called_once_with('123')
-
-
-def test_read_rocket_server_error():
- with patch.object(
- RocketController,
- 'get_rocket_by_id',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.get('/rockets/123')
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_update_rocket(stub_rocket):
- with patch.object(
- RocketController,
- 'update_rocket_by_id',
- return_value=RocketUpdated(rocket_id='123'),
- ) as mock_update_rocket:
- with patch.object(
- Motor, 'set_motor_kind', side_effect=None
- ) as mock_set_motor_kind:
- response = client.put(
- '/rockets/123',
- json=stub_rocket,
- params={'motor_kind': 'GENERIC'},
- )
- assert response.status_code == 200
- assert response.json() == {
- 'rocket_id': '123',
- 'message': 'Rocket successfully updated',
- }
- mock_update_rocket.assert_called_once_with(
- '123', Rocket(**stub_rocket)
- )
- mock_set_motor_kind.assert_called_once_with(MotorKinds.GENERIC)
-
-
-def test_update_rocket_invalid_input():
- response = client.put(
- '/rockets/123',
- json={'mass': 'foo', 'radius': 'bar'},
- params={'motor_kind': 'GENERIC'},
- )
- assert response.status_code == 422
-
-
-def test_update_rocket_not_found(stub_rocket):
- with patch.object(
- RocketController,
- 'update_rocket_by_id',
- side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND),
- ):
- response = client.put(
- '/rockets/123', json=stub_rocket, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 404
- assert response.json() == {'detail': 'Not Found'}
-
-
-def test_update_rocket_server_error(stub_rocket):
- with patch.object(
- RocketController,
- 'update_rocket_by_id',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.put(
- '/rockets/123', json=stub_rocket, params={'motor_kind': 'HYBRID'}
- )
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_delete_rocket():
- with patch.object(
- RocketController,
- 'delete_rocket_by_id',
- return_value=RocketDeleted(rocket_id='123'),
- ) as mock_delete_rocket:
- response = client.delete('/rockets/123')
- assert response.status_code == 200
- assert response.json() == {
- 'rocket_id': '123',
- 'message': 'Rocket successfully deleted',
- }
- mock_delete_rocket.assert_called_once_with('123')
-
-
-def test_delete_rocket_server_error():
- with patch.object(
- RocketController,
- 'delete_rocket_by_id',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.delete('/rockets/123')
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_simulate_rocket(stub_rocket_summary):
- with patch.object(
- RocketController,
- 'simulate_rocket',
- return_value=RocketSummary(**stub_rocket_summary),
- ) as mock_simulate_rocket:
- response = client.get('/rockets/123/summary')
- assert response.status_code == 200
- assert response.json() == stub_rocket_summary
- mock_simulate_rocket.assert_called_once_with('123')
-
-
-def test_simulate_rocket_not_found():
- with patch.object(
- RocketController,
- 'simulate_rocket',
- side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND),
- ) as mock_simulate_rocket:
- response = client.get('/rockets/123/summary')
- assert response.status_code == 404
- assert response.json() == {'detail': 'Not Found'}
- mock_simulate_rocket.assert_called_once_with('123')
-
-
-def test_simulate_rocket_server_error():
- with patch.object(
- RocketController,
- 'simulate_rocket',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.get('/rockets/123/summary')
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
-
-
-def test_read_rocketpy_rocket():
- with patch.object(
- RocketController,
- 'get_rocketpy_rocket_binary',
- return_value=b'rocketpy',
- ) as mock_read_rocketpy_rocket:
- response = client.get('/rockets/123/rocketpy')
- assert response.status_code == 203
- assert response.content == b'rocketpy'
- assert response.headers['content-type'] == 'application/octet-stream'
- mock_read_rocketpy_rocket.assert_called_once_with('123')
-
-
-def test_read_rocketpy_rocket_not_found():
- with patch.object(
- RocketController,
- 'get_rocketpy_rocket_binary',
- side_effect=HTTPException(status_code=status.HTTP_404_NOT_FOUND),
- ) as mock_read_rocketpy_rocket:
- response = client.get('/rockets/123/rocketpy')
- assert response.status_code == 404
- assert response.json() == {'detail': 'Not Found'}
- mock_read_rocketpy_rocket.assert_called_once_with('123')
-
-
-def test_read_rocketpy_rocket_server_error():
- with patch.object(
- RocketController,
- 'get_rocketpy_rocket_binary',
- side_effect=HTTPException(
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
- ),
- ):
- response = client.get('/rockets/123/rocketpy')
- assert response.status_code == 500
- assert response.json() == {'detail': 'Internal Server Error'}
diff --git a/tests/unit/test_controllers/test_controller_interface.py b/tests/unit/test_controllers/test_controller_interface.py
new file mode 100644
index 0000000..9cfec3f
--- /dev/null
+++ b/tests/unit/test_controllers/test_controller_interface.py
@@ -0,0 +1,227 @@
+from unittest.mock import patch, Mock
+import pytest
+from pymongo.errors import PyMongoError
+from fastapi import HTTPException, status
+from src.controllers.interface import (
+ ControllerBase,
+ controller_exception_handler,
+)
+
+
+@pytest.fixture
+def stub_model():
+ model = Mock()
+ model.NAME = 'test_model'
+ model.METHODS = ('GET', 'POST', 'PUT', 'DELETE')
+ model.UPDATED = lambda: 'Updated'
+ model.DELETED = lambda: 'Deleted'
+ model.CREATED = lambda arg: 'Created'
+ model.RETRIEVED = lambda arg: 'Retrieved'
+ return model
+
+
+@pytest.fixture
+def stub_controller(stub_model):
+ return ControllerBase([stub_model])
+
+
+@pytest.mark.asyncio
+async def test_controller_exception_handler_no_exception(stub_model):
+ async def method(
+ self, model, *args, **kwargs
+ ): # pylint: disable=unused-argument
+ return stub_model, args, kwargs
+
+ test_controller = Mock(method=method)
+ method = test_controller.method
+ mock_kwargs = {'foo': 'bar'}
+ mock_args = ('foo', 'bar')
+ wrapped_method = controller_exception_handler(method)
+ with patch('src.controllers.interface.logger') as mock_logger:
+ assert await wrapped_method(
+ test_controller, stub_model, *mock_args, **mock_kwargs
+ ) == (stub_model, mock_args, mock_kwargs)
+ mock_logger.error.assert_not_called()
+ mock_logger.info.assert_called_once_with(
+ "Call to method completed for Mock"
+ )
+
+
+@pytest.mark.asyncio
+async def test_controller_exception_handler_db_exception(stub_model):
+ async def method(self, model, *args, **kwargs):
+ raise PyMongoError
+
+ wrapped_method = controller_exception_handler(method)
+ with patch('src.controllers.interface.logger') as mock_logger:
+ with pytest.raises(HTTPException) as exc:
+ await wrapped_method(None, stub_model)
+ assert exc.value.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
+ assert exc.value.detail == "Operation failed, please try again later"
+ mock_logger.error.assert_called_once_with(
+ f"{method.__name__}: PyMongoError"
+ )
+
+
+@pytest.mark.asyncio
+async def test_controller_exception_handler_http_exception(stub_model):
+ async def method(self, model, *args, **kwargs):
+ raise HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND, detail='Not Found'
+ )
+
+ wrapped_method = controller_exception_handler(method)
+ with patch('src.controllers.interface.logger') as mock_logger:
+ with pytest.raises(HTTPException) as exc:
+ await wrapped_method(None, stub_model)
+ assert exc.value.status_code == status.HTTP_404_NOT_FOUND
+ assert exc.value.detail == 'Not Found'
+ mock_logger.error.assert_not_called()
+
+
+@pytest.mark.asyncio
+async def test_controller_exception_handler_unexpected_exception(stub_model):
+ async def method(self, model, *args, **kwargs):
+ raise ValueError('Test Error')
+
+ wrapped_method = controller_exception_handler(method)
+ with patch('src.controllers.interface.logger') as mock_logger:
+ with pytest.raises(HTTPException) as exc:
+ await wrapped_method(None, stub_model)
+ assert exc.value.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
+ assert exc.value.detail == 'Unexpected error'
+ mock_logger.exception.assert_called_once_with(
+ f"{method.__name__}: Unexpected error Test Error"
+ )
+
+
+def test_controller_interface_init(stub_model):
+ with patch(
+ 'src.controllers.interface.ControllerBase._generate_method'
+ ) as mock_gen:
+ mock_gen.return_value = lambda *args, **kwargs: True
+ stub_controller = ControllerBase([stub_model])
+ assert stub_controller._initialized_models == {
+ 'test_model': stub_model
+ }
+ assert hasattr(stub_controller, 'post_test_model')
+ assert hasattr(stub_controller, 'get_test_model_by_id')
+ assert hasattr(stub_controller, 'put_test_model_by_id')
+ assert hasattr(stub_controller, 'delete_test_model_by_id')
+ assert getattr(stub_controller, 'post_test_model')() is True
+ assert getattr(stub_controller, 'get_test_model_by_id')() is True
+ assert getattr(stub_controller, 'put_test_model_by_id')() is True
+ assert getattr(stub_controller, 'delete_test_model_by_id')() is True
+
+
+@pytest.mark.asyncio
+async def test_controller_interface_generate_available_method(
+ stub_controller, stub_model
+):
+ with patch('src.controllers.interface.RepositoryInterface') as mock_repo:
+ with patch(
+ 'src.controllers.interface.ControllerBase._get_model'
+ ) as mock_get:
+ mock_get.return_value = stub_model
+ method = stub_controller._generate_method('get', stub_model)
+ assert (
+ await method(None, stub_model, mock_repo, 'arg', key='bar')
+ == stub_model
+ )
+
+
+@pytest.mark.asyncio
+async def test_controller_interface_generate_unavailable_method(
+ stub_controller, stub_model
+):
+ with patch('src.controllers.interface.RepositoryInterface') as mock_repo:
+ with pytest.raises(NotImplementedError):
+ method = stub_controller._generate_method('foo', stub_model)
+ await method(None, stub_model, mock_repo, 'arg', key='bar')
+
+
+@pytest.mark.asyncio
+async def test_controller_interface_post_model(stub_controller, stub_model):
+ with patch('src.controllers.interface.RepositoryInterface') as mock_repo:
+ with patch(
+ 'src.controllers.interface.RepositoryInterface.get_model_repo'
+ ) as mock_get_repo:
+ mock_get_repo.return_value = mock_repo
+ assert (
+ await stub_controller._post_model(
+ stub_model, mock_repo, stub_model
+ )
+ == 'Created'
+ )
+
+
+@pytest.mark.asyncio
+async def test_controller_interface_get_model_found(
+ stub_controller, stub_model
+):
+ with patch('src.controllers.interface.RepositoryInterface') as mock_repo:
+ with patch(
+ 'src.controllers.interface.RepositoryInterface.get_model_repo'
+ ) as mock_get_repo:
+ mock_get_repo.return_value = mock_repo
+ mock_repo.return_value.__aenter__.return_value.read_test_model_by_id.return_value = (
+ stub_model
+ )
+ assert (
+ await stub_controller._get_model(stub_model, mock_repo, '123')
+ == 'Retrieved'
+ )
+
+
+@pytest.mark.asyncio
+async def test_controller_interface_get_model_not_found(
+ stub_controller, stub_model
+):
+ with patch('src.controllers.interface.RepositoryInterface') as mock_repo:
+ with patch(
+ 'src.controllers.interface.RepositoryInterface.get_model_repo'
+ ) as mock_get_repo:
+ mock_get_repo.return_value = mock_repo
+ mock_repo.return_value.__aenter__.return_value.read_test_model_by_id.return_value = (
+ None
+ )
+ with pytest.raises(HTTPException) as exc:
+ await stub_controller._get_model(stub_model, mock_repo, '123')
+ assert exc.value.status_code == status.HTTP_404_NOT_FOUND
+ assert exc.value.detail == 'test_model not found'
+
+
+@pytest.mark.asyncio
+async def test_controller_interface_update_model(stub_controller, stub_model):
+ with patch('src.controllers.interface.RepositoryInterface') as mock_repo:
+ with patch(
+ 'src.controllers.interface.RepositoryInterface.get_model_repo'
+ ) as mock_get_repo:
+ mock_get_repo.return_value = mock_repo
+ mock_repo.return_value.__aenter__.return_value.update_test_model_by_id.return_value = (
+ None
+ )
+ assert (
+ await stub_controller._put_model(
+ stub_model, mock_repo, '123', stub_model
+ )
+ == 'Updated'
+ )
+
+
+@pytest.mark.asyncio
+async def test_controller_interface_delete_model(stub_controller, stub_model):
+ with patch('src.controllers.interface.RepositoryInterface') as mock_repo:
+ with patch(
+ 'src.controllers.interface.RepositoryInterface.get_model_repo'
+ ) as mock_get_repo:
+ mock_get_repo.return_value = mock_repo
+ mock_repo.return_value.__aenter__.return_value.delete_test_model_by_id.return_value = (
+ None
+ )
+ assert (
+ await stub_controller._delete_model(
+ stub_model, mock_repo, '123'
+ )
+ == 'Deleted'
+ )
diff --git a/tests/unit/test_repositories/test_repository_interface.py b/tests/unit/test_repositories/test_repository_interface.py
new file mode 100644
index 0000000..f4989dc
--- /dev/null
+++ b/tests/unit/test_repositories/test_repository_interface.py
@@ -0,0 +1,286 @@
+from unittest.mock import patch, Mock, AsyncMock
+import pytest
+import pytest_asyncio
+from pymongo.errors import PyMongoError
+from fastapi import HTTPException, status
+from src.repositories.interface import (
+ RepositoryInterface,
+ repository_exception_handler,
+ RepositoryNotInitializedException,
+)
+
+
+@pytest.fixture
+def stub_loaded_model():
+ return {'_id': 'mock_id'}
+
+
+@pytest.fixture
+def mock_model():
+ return Mock(set_id=Mock())
+
+
+@pytest_asyncio.fixture
+def mock_db_interface(stub_loaded_model):
+ async def async_gen(*args, **kwargs): # pylint: disable=unused-argument
+ yield stub_loaded_model
+ yield stub_loaded_model
+
+ mock_db_interface = AsyncMock()
+ mock_db_interface.find = Mock(return_value=async_gen())
+ mock_db_interface.insert_one = AsyncMock(
+ return_value=Mock(inserted_id='mock_id')
+ )
+ mock_db_interface.update_one = AsyncMock(return_value=None)
+ mock_db_interface.find_one = AsyncMock(return_value=stub_loaded_model)
+ mock_db_interface.delete_one = AsyncMock(return_value=None)
+ return mock_db_interface
+
+
+@pytest_asyncio.fixture
+def mock_db_interface_empty_find():
+ async def async_gen(*args, **kwargs): # pylint: disable=unused-argument
+ if False: # pylint: disable=using-constant-test
+ yield
+
+ mock_db_interface = AsyncMock()
+ mock_db_interface.find = Mock(return_value=async_gen())
+ return mock_db_interface
+
+
+@pytest_asyncio.fixture
+def stub_repository(mock_model):
+ with patch.object(RepositoryInterface, "_initialize", return_value=None):
+ stub_model = Mock(
+ NAME='mock_model',
+ model_validate=mock_model,
+ )
+ repo = RepositoryInterface(stub_model)
+ repo._initialized = True
+ yield repo
+
+
+@pytest_asyncio.fixture
+def stub_repository_invalid_model():
+ with patch.object(RepositoryInterface, "_initialize", return_value=None):
+ stub_model = Mock(
+ NAME='mock_model',
+ model_validate=Mock(return_value=False),
+ )
+ repo = RepositoryInterface(stub_model)
+ repo._initialized = True
+ yield repo
+
+
+@pytest.mark.asyncio
+async def test_repository_exception_handler_no_exception():
+ async def method(self, *args, **kwargs): # pylint: disable=unused-argument
+ return args, kwargs
+
+ mock_kwargs = {'foo': 'bar'}
+ mock_args = ('foo', 'bar')
+ mock_repo = Mock(model=Mock(NAME='mock_model'))
+ wrapped_method = repository_exception_handler(method)
+ with patch('src.repositories.interface.logger') as mock_logger:
+ assert await wrapped_method(mock_repo, *mock_args, **mock_kwargs) == (
+ mock_args,
+ mock_kwargs,
+ )
+ mock_logger.error.assert_not_called()
+ mock_logger.info.assert_called_once_with(
+ f"Call to repositories.{mock_repo.model.NAME}.{method.__name__} completed for {mock_kwargs}"
+ )
+
+
+@pytest.mark.asyncio
+async def test_repository_exception_handler_db_exception():
+ async def method(self, *args, **kwargs):
+ raise PyMongoError
+
+ wrapped_method = repository_exception_handler(method)
+ with pytest.raises(PyMongoError):
+ await wrapped_method(Mock(model=Mock(NAME='mock_model')))
+
+
+@pytest.mark.asyncio
+async def test_repository_exception_not_initialized_exception():
+ async def method(self, *args, **kwargs):
+ raise RepositoryNotInitializedException
+
+ wrapped_method = repository_exception_handler(method)
+ with pytest.raises(HTTPException) as exc:
+ await wrapped_method(Mock(model=Mock(NAME='mock_model')))
+ assert exc.value.status_code == status.HTTP_503_SERVICE_UNAVAILABLE
+ assert (
+ exc.value.detail
+ == "Repository not initialized. Please try again later."
+ )
+
+
+@pytest.mark.asyncio
+async def test_repository_exception_handler_unexpected_exception():
+ async def method(self, *args, **kwargs):
+ raise Exception # pylint: disable=broad-exception-raised
+
+ wrapped_method = repository_exception_handler(method)
+ with pytest.raises(HTTPException) as exc:
+ await wrapped_method(Mock(model=Mock(NAME='mock_model')))
+ assert exc.value.status_code == status.HTTP_500_INTERNAL_SERVER_ERROR
+ assert exc.value.detail == 'Unexpected error ocurred'
+
+
+@pytest.mark.asyncio
+async def test_repository_interface_init(stub_repository):
+ assert stub_repository.model.NAME == 'mock_model'
+
+
+def test_get_model_repo(stub_repository):
+ with patch(
+ 'src.repositories.interface.importlib.import_module'
+ ) as mock_import_module:
+ mock_import_module.return_value = Mock(MockmodelRepository='mock_repo')
+ assert (
+ stub_repository.get_model_repo(Mock(NAME='mockmodel'))
+ == 'mock_repo'
+ )
+
+
+@pytest.mark.asyncio
+async def test_repository_insert_data(stub_repository, mock_db_interface):
+ with patch(
+ 'src.repositories.interface.RepositoryInterface.get_collection',
+ return_value=mock_db_interface,
+ ):
+ assert await stub_repository.insert('mock_data') == 'mock_id'
+ mock_db_interface.insert_one.assert_called_once_with('mock_data')
+
+
+@pytest.mark.asyncio
+async def test_repository_insert_invalid_data(stub_repository_invalid_model):
+ with patch(
+ 'src.repositories.interface.RepositoryInterface.get_collection',
+ return_value=mock_db_interface,
+ ):
+ with pytest.raises(HTTPException):
+ await stub_repository_invalid_model.insert('invalid_model_data')
+
+
+@pytest.mark.asyncio
+async def test_repository_update_data(stub_repository, mock_db_interface):
+ with patch(
+ 'src.repositories.interface.RepositoryInterface.get_collection',
+ return_value=mock_db_interface,
+ ):
+ with patch(
+ 'src.repositories.interface.ObjectId', side_effect=lambda x: x
+ ):
+ assert (
+ await stub_repository.update_by_id(
+ 'mock_data', data_id='mock_id'
+ )
+ is stub_repository
+ )
+ mock_db_interface.update_one.assert_called_once_with(
+ {'_id': 'mock_id'}, {'$set': 'mock_data'}
+ )
+
+
+@pytest.mark.asyncio
+async def test_repository_update_invalid_data(stub_repository_invalid_model):
+ with patch(
+ 'src.repositories.interface.RepositoryInterface.get_collection',
+ return_value=mock_db_interface,
+ ):
+ with pytest.raises(HTTPException):
+ await stub_repository_invalid_model.update_by_id(
+ data_id='mock_id', data='invalid_model_data'
+ )
+
+
+@pytest.mark.asyncio
+async def test_repository_find_data_found(
+ stub_repository, mock_db_interface, stub_loaded_model
+):
+ with patch(
+ 'src.repositories.interface.RepositoryInterface.get_collection',
+ return_value=mock_db_interface,
+ ):
+ with patch(
+ 'src.repositories.interface.ObjectId', side_effect=lambda x: x
+ ):
+ assert (
+ await stub_repository.find_by_id(data_id='mock_id')
+ == stub_repository.model.model_validate.return_value
+ )
+ mock_db_interface.find_one.assert_called_once_with(
+ {'_id': 'mock_id'}
+ )
+ stub_repository.model.model_validate.return_value.set_id.assert_called_once_with(
+ stub_loaded_model['_id']
+ )
+
+
+@pytest.mark.asyncio
+async def test_repository_find_data_not_found(
+ stub_repository, mock_db_interface
+):
+ mock_db_interface.find_one.return_value = None
+ with patch(
+ 'src.repositories.interface.RepositoryInterface.get_collection',
+ return_value=mock_db_interface,
+ ):
+ with patch(
+ 'src.repositories.interface.ObjectId', side_effect=lambda x: x
+ ):
+ assert await stub_repository.find_by_id(data_id='mock_id') is None
+ mock_db_interface.find_one.assert_called_once_with(
+ {'_id': 'mock_id'}
+ )
+
+
+@pytest.mark.asyncio
+async def test_repository_delete_data(stub_repository, mock_db_interface):
+ with patch(
+ 'src.repositories.interface.RepositoryInterface.get_collection',
+ return_value=mock_db_interface,
+ ):
+ with patch(
+ 'src.repositories.interface.ObjectId', side_effect=lambda x: x
+ ):
+ assert (
+ await stub_repository.delete_by_id(data_id='mock_id')
+ is stub_repository
+ )
+ mock_db_interface.delete_one.assert_called_once_with(
+ {'_id': 'mock_id'}
+ )
+
+
+@pytest.mark.asyncio
+async def test_repository_find_by_query_found(
+ stub_repository, mock_db_interface, stub_loaded_model
+):
+ with patch(
+ 'src.repositories.interface.RepositoryInterface.get_collection',
+ return_value=mock_db_interface,
+ ):
+ assert await stub_repository.find_by_query('mock_query') == [
+ stub_repository.model.model_validate.return_value,
+ stub_repository.model.model_validate.return_value,
+ ]
+ mock_db_interface.find.assert_called_once_with('mock_query')
+ stub_repository.model.model_validate.return_value.set_id.assert_called_with(
+ stub_loaded_model['_id']
+ )
+
+
+@pytest.mark.asyncio
+async def test_repository_find_by_query_not_found(
+ stub_repository, mock_db_interface_empty_find
+):
+ with patch(
+ 'src.repositories.interface.RepositoryInterface.get_collection',
+ return_value=mock_db_interface_empty_find,
+ ):
+ assert await stub_repository.find_by_query('mock_query') == []
+ mock_db_interface_empty_find.find.assert_called_once_with('mock_query')
diff --git a/tests/test_routes/conftest.py b/tests/unit/test_routes/conftest.py
similarity index 60%
rename from tests/test_routes/conftest.py
rename to tests/unit/test_routes/conftest.py
index e8f6fb1..74c63f9 100644
--- a/tests/test_routes/conftest.py
+++ b/tests/unit/test_routes/conftest.py
@@ -1,35 +1,37 @@
import json
import pytest
-from lib.models.rocket import Rocket
-from lib.models.motor import Motor, MotorTank, TankFluids, TankKinds
-from lib.models.environment import Env
-from lib.models.aerosurfaces import Fins, NoseCone
+from src.models.rocket import RocketModel
+from src.models.sub.tanks import MotorTank, TankFluids, TankKinds
+from src.models.motor import MotorModel
+from src.models.environment import EnvironmentModel
+from src.models.sub.aerosurfaces import Fins, NoseCone
@pytest.fixture
-def stub_env():
- env = Env(latitude=0, longitude=0)
+def stub_environment_dump():
+ env = EnvironmentModel(latitude=0, longitude=0)
env_json = env.model_dump_json()
return json.loads(env_json)
@pytest.fixture
-def stub_motor():
- motor = Motor(
+def stub_motor_dump():
+ motor = MotorModel(
thrust_source=[[0, 0]],
burn_time=0,
nozzle_radius=0,
dry_mass=0,
dry_inertia=[0, 0, 0],
center_of_dry_mass_position=0,
+ motor_kind='GENERIC',
)
motor_json = motor.model_dump_json()
return json.loads(motor_json)
@pytest.fixture
-def stub_tank():
+def stub_tank_dump():
tank = MotorTank(
geometry=[[(0, 0), 0]],
gas=TankFluids(name='gas', density=0),
@@ -44,14 +46,14 @@ def stub_tank():
@pytest.fixture
-def stub_level_tank(stub_tank):
- stub_tank.update({'tank_kind': TankKinds.LEVEL, 'liquid_height': 0})
- return stub_tank
+def stub_level_tank_dump(stub_tank_dump):
+ stub_tank_dump.update({'tank_kind': TankKinds.LEVEL, 'liquid_height': 0})
+ return stub_tank_dump
@pytest.fixture
-def stub_mass_flow_tank(stub_tank):
- stub_tank.update(
+def stub_mass_flow_tank_dump(stub_tank_dump):
+ stub_tank_dump.update(
{
'tank_kind': TankKinds.MASS_FLOW,
'gas_mass_flow_rate_in': 0,
@@ -62,25 +64,25 @@ def stub_mass_flow_tank(stub_tank):
'initial_gas_mass': 0,
}
)
- return stub_tank
+ return stub_tank_dump
@pytest.fixture
-def stub_ullage_tank(stub_tank):
- stub_tank.update({'tank_kind': TankKinds.ULLAGE, 'ullage': 0})
- return stub_tank
+def stub_ullage_tank_dump(stub_tank_dump):
+ stub_tank_dump.update({'tank_kind': TankKinds.ULLAGE, 'ullage': 0})
+ return stub_tank_dump
@pytest.fixture
-def stub_mass_tank(stub_tank):
- stub_tank.update(
+def stub_mass_tank_dump(stub_tank_dump):
+ stub_tank_dump.update(
{'tank_kind': TankKinds.MASS, 'liquid_mass': 0, 'gas_mass': 0}
)
- return stub_tank
+ return stub_tank_dump
@pytest.fixture
-def stub_nose_cone():
+def stub_nose_cone_dump():
nose_cone = NoseCone(
name='nose',
length=0,
@@ -94,9 +96,9 @@ def stub_nose_cone():
@pytest.fixture
-def stub_fins():
+def stub_fins_dump():
fins = Fins(
- fins_kind='TRAPEZOIDAL',
+ fins_kind='trapezoidal',
name='fins',
n=0,
root_chord=0,
@@ -108,9 +110,9 @@ def stub_fins():
@pytest.fixture
-def stub_rocket(stub_motor, stub_nose_cone, stub_fins):
- rocket = Rocket(
- motor=stub_motor,
+def stub_rocket_dump(stub_motor_dump, stub_nose_cone_dump, stub_fins_dump):
+ rocket = RocketModel(
+ motor=stub_motor_dump,
radius=0,
mass=0,
motor_position=0,
@@ -118,9 +120,9 @@ def stub_rocket(stub_motor, stub_nose_cone, stub_fins):
inertia=[0, 0, 0],
power_off_drag=[(0, 0)],
power_on_drag=[(0, 0)],
- nose=stub_nose_cone,
- fins=[stub_fins],
- coordinate_system_orientation='TAIL_TO_NOSE',
+ nose=stub_nose_cone_dump,
+ fins=[stub_fins_dump],
+ coordinate_system_orientation='tail_to_nose',
)
rocket_json = rocket.model_dump_json()
return json.loads(rocket_json)
diff --git a/tests/unit/test_routes/test_environments_route.py b/tests/unit/test_routes/test_environments_route.py
new file mode 100644
index 0000000..93ff4c3
--- /dev/null
+++ b/tests/unit/test_routes/test_environments_route.py
@@ -0,0 +1,258 @@
+from unittest.mock import patch, Mock, AsyncMock
+import json
+import pytest
+from fastapi.testclient import TestClient
+from fastapi import HTTPException
+from src.models.environment import EnvironmentModel
+from src.views.environment import (
+ EnvironmentView,
+ EnvironmentCreated,
+ EnvironmentRetrieved,
+ EnvironmentSimulation,
+)
+from src import app
+
+client = TestClient(app)
+
+
+@pytest.fixture
+def stub_environment_simulation_dump():
+ env_simulation = EnvironmentSimulation()
+ env_simulation_json = env_simulation.model_dump_json()
+ return json.loads(env_simulation_json)
+
+
+@pytest.fixture(autouse=True)
+def mock_controller_instance():
+ with patch(
+ "src.routes.environment.EnvironmentController", autospec=True
+ ) as mock_controller:
+ mock_controller_instance = mock_controller.return_value
+ mock_controller_instance.post_environment = Mock()
+ mock_controller_instance.get_environment_by_id = Mock()
+ mock_controller_instance.put_environment_by_id = Mock()
+ mock_controller_instance.delete_environment_by_id = Mock()
+ mock_controller_instance.get_environment_simulation = Mock()
+ mock_controller_instance.get_rocketpy_environment_binary = Mock()
+ yield mock_controller_instance
+
+
+def test_create_environment(stub_environment_dump, mock_controller_instance):
+ mock_response = AsyncMock(
+ return_value=EnvironmentCreated(environment_id='123')
+ )
+ mock_controller_instance.post_environment = mock_response
+ response = client.post('/environments/', json=stub_environment_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'environment_id': '123',
+ 'message': 'Environment successfully created',
+ }
+ mock_controller_instance.post_environment.assert_called_once_with(
+ EnvironmentModel(**stub_environment_dump)
+ )
+
+
+def test_create_environment_optional_params(
+ stub_environment_dump, mock_controller_instance
+):
+ stub_environment_dump.update(
+ {
+ 'latitude': 0,
+ 'longitude': 0,
+ 'elevation': 1,
+ 'atmospheric_model_type': 'standard_atmosphere',
+ 'atmospheric_model_file': None,
+ 'date': '2021-01-01T00:00:00',
+ }
+ )
+ mock_response = AsyncMock(
+ return_value=EnvironmentCreated(environment_id='123')
+ )
+ mock_controller_instance.post_environment = mock_response
+ response = client.post('/environments/', json=stub_environment_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'environment_id': '123',
+ 'message': 'Environment successfully created',
+ }
+
+
+def test_create_environment_invalid_input():
+ response = client.post(
+ '/environments/', json={'latitude': 'foo', 'longitude': 'bar'}
+ )
+ assert response.status_code == 422
+
+
+def test_create_environment_server_error(
+ stub_environment_dump, mock_controller_instance
+):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=500))
+ mock_controller_instance.post_environment = mock_response
+ response = client.post('/environments/', json=stub_environment_dump)
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_read_environment(stub_environment_dump, mock_controller_instance):
+ environment_view = EnvironmentView(
+ environment_id='123', **stub_environment_dump
+ )
+ mock_response = AsyncMock(
+ return_value=EnvironmentRetrieved(environment=environment_view)
+ )
+ mock_controller_instance.get_environment_by_id = mock_response
+ response = client.get('/environments/123')
+ assert response.status_code == 200
+ assert response.json() == {
+ 'message': 'Environment successfully retrieved',
+ 'environment': json.loads(environment_view.model_dump_json()),
+ }
+ mock_controller_instance.get_environment_by_id.assert_called_once_with(
+ '123'
+ )
+
+
+def test_read_environment_not_found(mock_controller_instance):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=404))
+ mock_controller_instance.get_environment_by_id = mock_response
+ response = client.get('/environments/123')
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+ mock_controller_instance.get_environment_by_id.assert_called_once_with(
+ '123'
+ )
+
+
+def test_read_environment_server_error(mock_controller_instance):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=500))
+ mock_controller_instance.get_environment_by_id = mock_response
+ response = client.get('/environments/123')
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_update_environment(stub_environment_dump, mock_controller_instance):
+ mock_reponse = AsyncMock(return_value=None)
+ mock_controller_instance.put_environment_by_id = mock_reponse
+ response = client.put('/environments/123', json=stub_environment_dump)
+ assert response.status_code == 204
+ mock_controller_instance.put_environment_by_id.assert_called_once_with(
+ '123', EnvironmentModel(**stub_environment_dump)
+ )
+
+
+def test_update_environment_invalid_input():
+ response = client.put(
+ '/environments/123', json={'consignment': 'foo', 'delivery': 'bar'}
+ )
+ assert response.status_code == 422
+
+
+def test_update_environment_not_found(
+ stub_environment_dump, mock_controller_instance
+):
+ mock_reponse = AsyncMock(side_effect=HTTPException(status_code=404))
+ mock_controller_instance.put_environment_by_id = mock_reponse
+ response = client.put('/environments/123', json=stub_environment_dump)
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+ mock_controller_instance.put_environment_by_id.assert_called_once_with(
+ '123', EnvironmentModel(**stub_environment_dump)
+ )
+
+
+def test_update_environment_server_error(
+ stub_environment_dump, mock_controller_instance
+):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=500))
+ mock_controller_instance.put_environment_by_id = mock_response
+ response = client.put('/environments/123', json=stub_environment_dump)
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_delete_environment(mock_controller_instance):
+ mock_reponse = AsyncMock(return_value=None)
+ mock_controller_instance.delete_environment_by_id = mock_reponse
+ response = client.delete('/environments/123')
+ assert response.status_code == 204
+ mock_controller_instance.delete_environment_by_id.assert_called_once_with(
+ '123'
+ )
+
+
+def test_delete_environment_server_error(mock_controller_instance):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=500))
+ mock_controller_instance.delete_environment_by_id = mock_response
+ response = client.delete('/environments/123')
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_get_environment_simulation_success(
+ stub_environment_simulation_dump, mock_controller_instance
+):
+ mock_reponse = AsyncMock(
+ return_value=EnvironmentSimulation(**stub_environment_simulation_dump)
+ )
+ mock_controller_instance.get_environment_simulation = mock_reponse
+ response = client.get('/environments/123/simulate')
+ assert response.status_code == 200
+ assert response.json() == stub_environment_simulation_dump
+ mock_controller_instance.get_environment_simulation.assert_called_once_with(
+ '123'
+ )
+
+
+def test_get_environment_simulation_not_found(mock_controller_instance):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=404))
+ mock_controller_instance.get_environment_simulation = mock_response
+ response = client.get('/environments/123/simulate')
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+ mock_controller_instance.get_environment_simulation.assert_called_once_with(
+ '123'
+ )
+
+
+def test_get_environment_simulation_server_error(mock_controller_instance):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=500))
+ mock_controller_instance.get_environment_simulation = mock_response
+ response = client.get('/environments/123/simulate')
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_read_rocketpy_environment_binary(mock_controller_instance):
+ mock_response = AsyncMock(return_value=b'rocketpy')
+ mock_controller_instance.get_rocketpy_environment_binary = mock_response
+ response = client.get('/environments/123/rocketpy')
+ assert response.status_code == 200
+ assert response.content == b'rocketpy'
+ assert response.headers['content-type'] == 'application/octet-stream'
+ mock_controller_instance.get_rocketpy_environment_binary.assert_called_once_with(
+ '123'
+ )
+
+
+def test_read_rocketpy_environment_binary_not_found(mock_controller_instance):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=404))
+ mock_controller_instance.get_rocketpy_environment_binary = mock_response
+ response = client.get('/environments/123/rocketpy')
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+ mock_controller_instance.get_rocketpy_environment_binary.assert_called_once_with(
+ '123'
+ )
+
+
+def test_read_rocketpy_environment_binary_server_error(
+ mock_controller_instance,
+):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=500))
+ mock_controller_instance.get_rocketpy_environment_binary = mock_response
+ response = client.get('/environments/123/rocketpy')
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
diff --git a/tests/unit/test_routes/test_flights_route.py b/tests/unit/test_routes/test_flights_route.py
new file mode 100644
index 0000000..0b8ac30
--- /dev/null
+++ b/tests/unit/test_routes/test_flights_route.py
@@ -0,0 +1,365 @@
+from unittest.mock import patch, Mock, AsyncMock
+import json
+import pytest
+from fastapi.testclient import TestClient
+from fastapi import HTTPException, status
+from src.models.environment import EnvironmentModel
+from src.models.flight import FlightModel
+from src.models.rocket import RocketModel
+from src.views.motor import MotorView
+from src.views.rocket import RocketView
+from src.views.flight import (
+ FlightCreated,
+ FlightRetrieved,
+ FlightSimulation,
+ FlightView,
+)
+from src import app
+
+client = TestClient(app)
+
+
+@pytest.fixture
+def stub_flight_dump(stub_environment_dump, stub_rocket_dump):
+ flight = {
+ 'name': 'Test Flight',
+ 'environment': stub_environment_dump,
+ 'rocket': stub_rocket_dump,
+ 'rail_length': 1,
+ 'time_overshoot': True,
+ 'terminate_on_apogee': True,
+ 'equations_of_motion': 'standard',
+ }
+ return flight
+
+
+@pytest.fixture
+def stub_flight_simulate_dump():
+ flight_simulate = FlightSimulation()
+ flight_simulate_json = flight_simulate.model_dump_json()
+ return json.loads(flight_simulate_json)
+
+
+@pytest.fixture(autouse=True)
+def mock_controller_instance():
+ with patch(
+ "src.routes.flight.FlightController", autospec=True
+ ) as mock_controller:
+ mock_controller_instance = mock_controller.return_value
+ mock_controller_instance.post_flight = Mock()
+ mock_controller_instance.get_flight_by_id = Mock()
+ mock_controller_instance.put_flight_by_id = Mock()
+ mock_controller_instance.delete_flight_by_id = Mock()
+ mock_controller_instance.get_flight_simulation = Mock()
+ mock_controller_instance.get_rocketpy_flight_binary = Mock()
+ mock_controller_instance.update_environment_by_flight_id = Mock()
+ mock_controller_instance.update_rocket_by_flight_id = Mock()
+ yield mock_controller_instance
+
+
+def test_create_flight(stub_flight_dump, mock_controller_instance):
+ mock_response = AsyncMock(return_value=FlightCreated(flight_id='123'))
+ mock_controller_instance.post_flight = mock_response
+ response = client.post('/flights/', json=stub_flight_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'flight_id': '123',
+ 'message': 'Flight successfully created',
+ }
+ mock_controller_instance.post_flight.assert_called_once_with(
+ FlightModel(**stub_flight_dump)
+ )
+
+
+def test_create_flight_optional_params(
+ stub_flight_dump, mock_controller_instance
+):
+ stub_flight_dump.update(
+ {
+ 'inclination': 0,
+ 'heading': 0,
+ 'max_time': 1,
+ 'max_time_step': 1.0,
+ 'min_time_step': 1,
+ 'rtol': 1.0,
+ 'atol': 1.0,
+ 'verbose': True,
+ }
+ )
+ mock_response = AsyncMock(return_value=FlightCreated(flight_id='123'))
+ mock_controller_instance.post_flight = mock_response
+ response = client.post('/flights/', json=stub_flight_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'flight_id': '123',
+ 'message': 'Flight successfully created',
+ }
+ mock_controller_instance.post_flight.assert_called_once_with(
+ FlightModel(**stub_flight_dump)
+ )
+
+
+def test_create_flight_invalid_input():
+ response = client.post(
+ '/flights/', json={'environment': 'foo', 'rocket': 'bar'}
+ )
+ assert response.status_code == 422
+
+
+def test_create_flight_server_error(
+ stub_flight_dump, mock_controller_instance
+):
+ mock_response = AsyncMock(
+ side_effect=HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+ )
+ mock_controller_instance.post_flight = mock_response
+ response = client.post('/flights/', json=stub_flight_dump)
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_read_flight(
+ stub_flight_dump,
+ stub_rocket_dump,
+ stub_motor_dump,
+ mock_controller_instance,
+):
+ del stub_rocket_dump['motor']
+ del stub_flight_dump['rocket']
+ stub_motor_dump.update({'motor_kind': 'SOLID'})
+ motor_view = MotorView(**stub_motor_dump)
+ rocket_view = RocketView(**stub_rocket_dump, motor=motor_view)
+ flight_view = FlightView(
+ **stub_flight_dump, flight_id='123', rocket=rocket_view
+ )
+ mock_response = AsyncMock(return_value=FlightRetrieved(flight=flight_view))
+ mock_controller_instance.get_flight_by_id = mock_response
+ response = client.get('/flights/123')
+ assert response.status_code == 200
+ assert response.json() == {
+ 'message': 'Flight successfully retrieved',
+ 'flight': json.loads(flight_view.model_dump_json()),
+ }
+ mock_controller_instance.get_flight_by_id.assert_called_once_with('123')
+
+
+def test_read_flight_not_found(mock_controller_instance):
+ mock_controller_instance.get_flight_by_id.side_effect = HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND
+ )
+ response = client.get('/flights/123')
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+
+
+def test_read_flight_server_error(mock_controller_instance):
+ mock_controller_instance.get_flight_by_id.side_effect = HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+ response = client.get('/flights/123')
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_update_flight_by_id(stub_flight_dump, mock_controller_instance):
+ mock_response = AsyncMock(return_value=None)
+ mock_controller_instance.put_flight_by_id = mock_response
+ response = client.put('/flights/123', json=stub_flight_dump)
+ assert response.status_code == 204
+ mock_controller_instance.put_flight_by_id.assert_called_once_with(
+ '123', FlightModel(**stub_flight_dump)
+ )
+
+
+def test_update_environment_by_flight_id(
+ stub_environment_dump, mock_controller_instance
+):
+ mock_response = AsyncMock(return_value=None)
+ mock_controller_instance.update_environment_by_flight_id = mock_response
+ response = client.put(
+ '/flights/123/environment', json=stub_environment_dump
+ )
+ assert response.status_code == 204
+ mock_controller_instance.update_environment_by_flight_id.assert_called_once_with(
+ '123', environment=EnvironmentModel(**stub_environment_dump)
+ )
+
+
+def test_update_rocket_by_flight_id(
+ stub_rocket_dump, mock_controller_instance
+):
+ mock_response = AsyncMock(return_value=None)
+ mock_controller_instance.update_rocket_by_flight_id = mock_response
+ response = client.put('/flights/123/rocket', json=stub_rocket_dump)
+ assert response.status_code == 204
+ mock_controller_instance.update_rocket_by_flight_id.assert_called_once_with(
+ '123', rocket=RocketModel(**stub_rocket_dump)
+ )
+
+
+def test_update_environment_by_flight_id_invalid_input():
+ response = client.put('/flights/123', json={'environment': 'foo'})
+ assert response.status_code == 422
+
+
+def test_update_rocket_by_flight_id_invalid_input():
+ response = client.put('/flights/123', json={'rocket': 'bar'})
+ assert response.status_code == 422
+
+
+def test_update_flight_invalid_input():
+ response = client.put(
+ '/flights/123', json={'environment': 'foo', 'rocket': 'bar'}
+ )
+ assert response.status_code == 422
+
+
+def test_update_flight_not_found(stub_flight_dump, mock_controller_instance):
+ mock_controller_instance.put_flight_by_id.side_effect = HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND
+ )
+ response = client.put('/flights/123', json=stub_flight_dump)
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+
+
+def test_update_flight_server_error(
+ stub_flight_dump, mock_controller_instance
+):
+ mock_controller_instance.put_flight_by_id.side_effect = HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+ response = client.put('/flights/123', json=stub_flight_dump)
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_update_environment_by_flight_id_not_found(
+ stub_environment_dump, mock_controller_instance
+):
+ mock_controller_instance.update_environment_by_flight_id.side_effect = (
+ HTTPException(status_code=status.HTTP_404_NOT_FOUND)
+ )
+ response = client.put(
+ '/flights/123/environment', json=stub_environment_dump
+ )
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+
+
+def test_update_environment_by_flight_id_server_error(
+ stub_environment_dump, mock_controller_instance
+):
+ mock_controller_instance.update_environment_by_flight_id.side_effect = (
+ HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
+ )
+ response = client.put(
+ '/flights/123/environment', json=stub_environment_dump
+ )
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_update_rocket_by_flight_id_not_found(
+ stub_rocket_dump, mock_controller_instance
+):
+ mock_controller_instance.update_rocket_by_flight_id.side_effect = (
+ HTTPException(status_code=status.HTTP_404_NOT_FOUND)
+ )
+ response = client.put('/flights/123/rocket', json=stub_rocket_dump)
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+
+
+def test_update_rocket_by_flight_id_server_error(
+ stub_rocket_dump, mock_controller_instance
+):
+ mock_controller_instance.update_rocket_by_flight_id.side_effect = (
+ HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
+ )
+ response = client.put('/flights/123/rocket', json=stub_rocket_dump)
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_delete_flight(mock_controller_instance):
+ mock_response = AsyncMock(return_value=None)
+ mock_controller_instance.delete_flight_by_id = mock_response
+ response = client.delete('/flights/123')
+ assert response.status_code == 204
+ mock_controller_instance.delete_flight_by_id.assert_called_once_with('123')
+
+
+def test_delete_flight_server_error(mock_controller_instance):
+ mock_controller_instance.delete_flight_by_id.side_effect = HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+ response = client.delete('/flights/123')
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_get_flight_simulation(
+ stub_flight_simulate_dump, mock_controller_instance
+):
+ mock_response = AsyncMock(
+ return_value=FlightSimulation(**stub_flight_simulate_dump)
+ )
+ mock_controller_instance.get_flight_simulation = mock_response
+ response = client.get('/flights/123/simulate')
+ assert response.status_code == 200
+ assert response.json() == stub_flight_simulate_dump
+ mock_controller_instance.get_flight_simulation.assert_called_once_with(
+ '123'
+ )
+
+
+def test_get_flight_simulation_not_found(mock_controller_instance):
+ mock_controller_instance.get_flight_simulation.side_effect = HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND
+ )
+ response = client.get('/flights/123/simulate')
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+
+
+def test_get_flight_simulation_server_error(mock_controller_instance):
+ mock_controller_instance.get_flight_simulation.side_effect = HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+ response = client.get('/flights/123/simulate')
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_read_rocketpy_flight_binary(mock_controller_instance):
+ mock_controller_instance.get_rocketpy_flight_binary = AsyncMock(
+ return_value=b'rocketpy'
+ )
+ response = client.get('/flights/123/rocketpy')
+ assert response.status_code == 200
+ assert response.content == b'rocketpy'
+ assert response.headers['content-type'] == 'application/octet-stream'
+ mock_controller_instance.get_rocketpy_flight_binary.assert_called_once_with(
+ '123'
+ )
+
+
+def test_read_rocketpy_flight_binary_not_found(mock_controller_instance):
+ mock_controller_instance.get_rocketpy_flight_binary.side_effect = (
+ HTTPException(status_code=status.HTTP_404_NOT_FOUND)
+ )
+ response = client.get('/flights/123/rocketpy')
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+
+
+def test_read_rocketpy_flight_binary_server_error(mock_controller_instance):
+ mock_controller_instance.get_rocketpy_flight_binary.side_effect = (
+ HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
+ )
+ response = client.get('/flights/123/rocketpy')
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
diff --git a/tests/unit/test_routes/test_motors_route.py b/tests/unit/test_routes/test_motors_route.py
new file mode 100644
index 0000000..e55c976
--- /dev/null
+++ b/tests/unit/test_routes/test_motors_route.py
@@ -0,0 +1,415 @@
+from unittest.mock import patch, AsyncMock, Mock
+import json
+import pytest
+from fastapi.testclient import TestClient
+from fastapi import HTTPException
+from src.models.motor import MotorModel
+from src.views.motor import (
+ MotorCreated,
+ MotorRetrieved,
+ MotorSimulation,
+ MotorView,
+)
+from src import app
+
+client = TestClient(app)
+
+
+@pytest.fixture
+def stub_motor_dump_simulation():
+ motor_simulation = MotorSimulation()
+ motor_simulation_json = motor_simulation.model_dump_json()
+ return json.loads(motor_simulation_json)
+
+
+@pytest.fixture(autouse=True)
+def mock_controller_instance():
+ with patch(
+ "src.routes.motor.MotorController", autospec=True
+ ) as mock_controller:
+ mock_controller_instance = mock_controller.return_value
+ mock_controller_instance.post_motor = Mock()
+ mock_controller_instance.get_motor_by_id = Mock()
+ mock_controller_instance.put_motor_by_id = Mock()
+ mock_controller_instance.delete_motor_by_id = Mock()
+ mock_controller_instance.get_motor_simulation = Mock()
+ mock_controller_instance.get_rocketpy_motor_binary = Mock()
+ yield mock_controller_instance
+
+
+def test_create_motor(stub_motor_dump, mock_controller_instance):
+ mock_response = AsyncMock(return_value=MotorCreated(motor_id='123'))
+ mock_controller_instance.post_motor = mock_response
+ response = client.post('/motors/', json=stub_motor_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'motor_id': '123',
+ 'message': 'Motor successfully created',
+ }
+ mock_controller_instance.post_motor.assert_called_once_with(
+ MotorModel(**stub_motor_dump)
+ )
+
+
+def test_create_motor_optional_params(
+ stub_motor_dump, mock_controller_instance
+):
+ stub_motor_dump.update(
+ {
+ 'interpolation_method': 'linear',
+ 'coordinate_system_orientation': 'nozzle_to_combustion_chamber',
+ 'reshape_thrust_curve': False,
+ }
+ )
+ mock_response = AsyncMock(return_value=MotorCreated(motor_id='123'))
+ mock_controller_instance.post_motor = mock_response
+ response = client.post('/motors/', json=stub_motor_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'motor_id': '123',
+ 'message': 'Motor successfully created',
+ }
+ mock_controller_instance.post_motor.assert_called_once_with(
+ MotorModel(**stub_motor_dump)
+ )
+
+
+def test_create_generic_motor(stub_motor_dump, mock_controller_instance):
+ stub_motor_dump.update(
+ {
+ 'chamber_radius': 0,
+ 'chamber_height': 0,
+ 'chamber_position': 0,
+ 'propellant_initial_mass': 0,
+ 'nozzle_position': 0,
+ 'motor_kind': 'GENERIC',
+ }
+ )
+ mock_response = AsyncMock(return_value=MotorCreated(motor_id='123'))
+ mock_controller_instance.post_motor = mock_response
+ response = client.post('/motors/', json=stub_motor_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'motor_id': '123',
+ 'message': 'Motor successfully created',
+ }
+ mock_controller_instance.post_motor.assert_called_once_with(
+ MotorModel(**stub_motor_dump)
+ )
+
+
+def test_create_liquid_motor_level_tank(
+ stub_motor_dump, stub_level_tank_dump, mock_controller_instance
+):
+ stub_motor_dump.update(
+ {'tanks': [stub_level_tank_dump], 'motor_kind': 'LIQUID'}
+ )
+ mock_response = AsyncMock(return_value=MotorCreated(motor_id='123'))
+ mock_controller_instance.post_motor = mock_response
+ response = client.post('/motors/', json=stub_motor_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'motor_id': '123',
+ 'message': 'Motor successfully created',
+ }
+ mock_controller_instance.post_motor.assert_called_once_with(
+ MotorModel(**stub_motor_dump)
+ )
+
+
+def test_create_liquid_motor_mass_flow_tank(
+ stub_motor_dump, stub_mass_flow_tank_dump, mock_controller_instance
+):
+ stub_motor_dump.update(
+ {'tanks': [stub_mass_flow_tank_dump], 'motor_kind': 'LIQUID'}
+ )
+ mock_response = AsyncMock(return_value=MotorCreated(motor_id='123'))
+ mock_controller_instance.post_motor = mock_response
+ response = client.post('/motors/', json=stub_motor_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'motor_id': '123',
+ 'message': 'Motor successfully created',
+ }
+ mock_controller_instance.post_motor.assert_called_once_with(
+ MotorModel(**stub_motor_dump)
+ )
+
+
+def test_create_liquid_motor_ullage_tank(
+ stub_motor_dump, stub_ullage_tank_dump, mock_controller_instance
+):
+ stub_motor_dump.update(
+ {'tanks': [stub_ullage_tank_dump], 'motor_kind': 'LIQUID'}
+ )
+ mock_response = AsyncMock(return_value=MotorCreated(motor_id='123'))
+ mock_controller_instance.post_motor = mock_response
+ response = client.post('/motors/', json=stub_motor_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'motor_id': '123',
+ 'message': 'Motor successfully created',
+ }
+ mock_controller_instance.post_motor.assert_called_once_with(
+ MotorModel(**stub_motor_dump)
+ )
+
+
+def test_create_liquid_motor_mass_tank(
+ stub_motor_dump, stub_mass_tank_dump, mock_controller_instance
+):
+ stub_motor_dump.update(
+ {'tanks': [stub_mass_tank_dump], 'motor_kind': 'LIQUID'}
+ )
+ mock_response = AsyncMock(return_value=MotorCreated(motor_id='123'))
+ mock_controller_instance.post_motor = mock_response
+ response = client.post('/motors/', json=stub_motor_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'motor_id': '123',
+ 'message': 'Motor successfully created',
+ }
+ mock_controller_instance.post_motor.assert_called_once_with(
+ MotorModel(**stub_motor_dump)
+ )
+
+
+def test_create_hybrid_motor(
+ stub_motor_dump, stub_level_tank_dump, mock_controller_instance
+):
+ stub_motor_dump.update(
+ {
+ 'grain_number': 0,
+ 'grain_density': 0,
+ 'grain_outer_radius': 0,
+ 'grain_initial_inner_radius': 0,
+ 'grain_initial_height': 0,
+ 'grains_center_of_mass_position': 0,
+ 'grain_separation': 0,
+ 'throat_radius': 0,
+ 'motor_kind': 'HYBRID',
+ 'tanks': [stub_level_tank_dump],
+ }
+ )
+ mock_response = AsyncMock(return_value=MotorCreated(motor_id='123'))
+ mock_controller_instance.post_motor = mock_response
+ response = client.post('/motors/', json=stub_motor_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'motor_id': '123',
+ 'message': 'Motor successfully created',
+ }
+ mock_controller_instance.post_motor.assert_called_once_with(
+ MotorModel(**stub_motor_dump)
+ )
+
+
+def test_create_solid_motor(stub_motor_dump, mock_controller_instance):
+ stub_motor_dump.update(
+ {
+ 'grain_number': 0,
+ 'grain_density': 0,
+ 'grain_outer_radius': 0,
+ 'grain_initial_inner_radius': 0,
+ 'grain_initial_height': 0,
+ 'grains_center_of_mass_position': 0,
+ 'grain_separation': 0,
+ 'motor_kind': 'SOLID',
+ }
+ )
+ mock_response = AsyncMock(return_value=MotorCreated(motor_id='123'))
+ mock_controller_instance.post_motor = mock_response
+ response = client.post('/motors/', json=stub_motor_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'motor_id': '123',
+ 'message': 'Motor successfully created',
+ }
+ mock_controller_instance.post_motor.assert_called_once_with(
+ MotorModel(**stub_motor_dump)
+ )
+
+
+def test_create_motor_invalid_input():
+ response = client.post(
+ '/motors/', json={'burn_time': 'foo', 'nozzle_radius': 'bar'}
+ )
+ assert response.status_code == 422
+
+
+def test_create_motor_invalid_input_post_instantiation(stub_motor_dump):
+ stub_motor_dump.update(
+ {
+ 'motor_kind': 'HYBRID',
+ }
+ )
+ response = client.post('/motors/', json=stub_motor_dump)
+ assert response.status_code == 422
+
+
+def test_create_motor_server_error(stub_motor_dump, mock_controller_instance):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=500))
+ mock_controller_instance.post_motor = mock_response
+ response = client.post('/motors/', json=stub_motor_dump)
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+ mock_controller_instance.post_motor.assert_called_once_with(
+ MotorModel(**stub_motor_dump)
+ )
+
+
+def test_read_motor(stub_motor_dump, mock_controller_instance):
+ motor_view = MotorView(motor_id='123', **stub_motor_dump)
+ mock_response = AsyncMock(return_value=MotorRetrieved(motor=motor_view))
+ mock_controller_instance.get_motor_by_id = mock_response
+ response = client.get('/motors/123')
+ assert response.status_code == 200
+ assert response.json() == {
+ 'message': 'Motor successfully retrieved',
+ 'motor': json.loads(motor_view.model_dump_json()),
+ }
+ mock_controller_instance.get_motor_by_id.assert_called_once_with('123')
+
+
+def test_read_motor_not_found(mock_controller_instance):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=404))
+ mock_controller_instance.get_motor_by_id = mock_response
+ response = client.get('/motors/123')
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+ mock_controller_instance.get_motor_by_id.assert_called_once_with('123')
+
+
+def test_read_motor_server_error(mock_controller_instance):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=500))
+ mock_controller_instance.get_motor_by_id = mock_response
+ response = client.get('/motors/123')
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_update_motor(stub_motor_dump, mock_controller_instance):
+ mock_response = AsyncMock(return_value=None)
+ mock_controller_instance.put_motor_by_id = mock_response
+ response = client.put('/motors/123', json=stub_motor_dump)
+ assert response.status_code == 204
+ mock_controller_instance.put_motor_by_id.assert_called_once_with(
+ '123', MotorModel(**stub_motor_dump)
+ )
+
+
+def test_update_motor_invalid_input():
+ response = client.put(
+ '/motors/123', json={'burn_time': 'foo', 'nozzle_radius': 'bar'}
+ )
+ assert response.status_code == 422
+
+
+def test_update_motor_not_found(stub_motor_dump, mock_controller_instance):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=404))
+ mock_controller_instance.put_motor_by_id = mock_response
+ response = client.put('/motors/123', json=stub_motor_dump)
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+ mock_controller_instance.put_motor_by_id.assert_called_once_with(
+ '123', MotorModel(**stub_motor_dump)
+ )
+
+
+def test_update_motor_server_error(stub_motor_dump, mock_controller_instance):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=500))
+ mock_controller_instance.put_motor_by_id = mock_response
+ response = client.put('/motors/123', json=stub_motor_dump)
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+ mock_controller_instance.put_motor_by_id.assert_called_once_with(
+ '123', MotorModel(**stub_motor_dump)
+ )
+
+
+def test_delete_motor(mock_controller_instance):
+ mock_response = AsyncMock(return_value=None)
+ mock_controller_instance.delete_motor_by_id = mock_response
+ response = client.delete('/motors/123')
+ assert response.status_code == 204
+ mock_controller_instance.delete_motor_by_id.assert_called_once_with('123')
+
+
+def test_delete_motor_server_error(mock_controller_instance):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=500))
+ mock_controller_instance.delete_motor_by_id = mock_response
+ response = client.delete('/motors/123')
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+ mock_controller_instance.delete_motor_by_id.assert_called_once_with('123')
+
+
+def test_get_motor_simulation(
+ mock_controller_instance, stub_motor_dump_simulation
+):
+ mock_response = AsyncMock(
+ return_value=MotorSimulation(**stub_motor_dump_simulation)
+ )
+ mock_controller_instance.get_motor_simulation = mock_response
+ response = client.get('/motors/123/simulate')
+ assert response.status_code == 200
+ assert response.json() == stub_motor_dump_simulation
+ mock_controller_instance.get_motor_simulation.assert_called_once_with(
+ '123'
+ )
+
+
+def test_get_motor_simulation_not_found(mock_controller_instance):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=404))
+ mock_controller_instance.get_motor_simulation = mock_response
+ response = client.get('/motors/123/simulate')
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+ mock_controller_instance.get_motor_simulation.assert_called_once_with(
+ '123'
+ )
+
+
+def test_get_motor_simulation_server_error(mock_controller_instance):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=500))
+ mock_controller_instance.get_motor_simulation = mock_response
+ response = client.get('/motors/123/simulate')
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+ mock_controller_instance.get_motor_simulation.assert_called_once_with(
+ '123'
+ )
+
+
+def test_read_rocketpy_motor_binary(mock_controller_instance):
+ mock_response = AsyncMock(return_value=b'rocketpy')
+ mock_controller_instance.get_rocketpy_motor_binary = mock_response
+ response = client.get('/motors/123/rocketpy')
+ assert response.status_code == 200
+ assert response.content == b'rocketpy'
+ assert response.headers['content-type'] == 'application/octet-stream'
+ mock_controller_instance.get_rocketpy_motor_binary.assert_called_once_with(
+ '123'
+ )
+
+
+def test_read_rocketpy_motor_binary_not_found(mock_controller_instance):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=404))
+ mock_controller_instance.get_rocketpy_motor_binary = mock_response
+ response = client.get('/motors/123/rocketpy')
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+ mock_controller_instance.get_rocketpy_motor_binary.assert_called_once_with(
+ '123'
+ )
+
+
+def test_read_rocketpy_motor_binary_server_error(mock_controller_instance):
+ mock_response = AsyncMock(side_effect=HTTPException(status_code=500))
+ mock_controller_instance.get_rocketpy_motor_binary = mock_response
+ response = client.get('/motors/123/rocketpy')
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+ mock_controller_instance.get_rocketpy_motor_binary.assert_called_once_with(
+ '123'
+ )
diff --git a/tests/unit/test_routes/test_rockets_route.py b/tests/unit/test_routes/test_rockets_route.py
new file mode 100644
index 0000000..6b73d8c
--- /dev/null
+++ b/tests/unit/test_routes/test_rockets_route.py
@@ -0,0 +1,472 @@
+from unittest.mock import patch, Mock, AsyncMock
+import json
+import pytest
+from fastapi.testclient import TestClient
+from fastapi import HTTPException, status
+from src.models.sub.aerosurfaces import (
+ Tail,
+ RailButtons,
+ Parachute,
+)
+from src.models.rocket import RocketModel
+from src.views.rocket import (
+ RocketCreated,
+ RocketRetrieved,
+ RocketSimulation,
+ RocketView,
+)
+from src import app
+
+client = TestClient(app)
+
+
+@pytest.fixture
+def stub_rocket_simulation_dump():
+ rocket_simulation = RocketSimulation()
+ rocket_simulation_json = rocket_simulation.model_dump_json()
+ return json.loads(rocket_simulation_json)
+
+
+@pytest.fixture
+def stub_tail_dump():
+ tail = Tail(
+ name='tail',
+ top_radius=0,
+ bottom_radius=0,
+ length=0,
+ position=0,
+ radius=0,
+ )
+ tail_json = tail.model_dump_json()
+ return json.loads(tail_json)
+
+
+@pytest.fixture
+def stub_rail_buttons_dump():
+ rail_buttons = RailButtons(
+ upper_button_position=0,
+ lower_button_position=0,
+ angular_position=0,
+ )
+ rail_buttons_json = rail_buttons.model_dump_json()
+ return json.loads(rail_buttons_json)
+
+
+@pytest.fixture
+def stub_parachute_dump():
+ parachute = Parachute(
+ name='parachute',
+ cd_s=0,
+ sampling_rate=1,
+ lag=0,
+ trigger='trigger',
+ noise=(0, 0, 0),
+ )
+ parachute_json = parachute.model_dump_json()
+ return json.loads(parachute_json)
+
+
+@pytest.fixture(autouse=True)
+def mock_controller_instance():
+ with patch(
+ "src.routes.rocket.RocketController", autospec=True
+ ) as mock_controller:
+ mock_controller_instance = mock_controller.return_value
+ mock_controller_instance.post_rocket = Mock()
+ mock_controller_instance.get_rocket_by_id = Mock()
+ mock_controller_instance.put_rocket_by_id = Mock()
+ mock_controller_instance.delete_rocket_by_id = Mock()
+ mock_controller_instance.get_rocket_simulation = Mock()
+ mock_controller_instance.get_rocketpy_rocket_binary = Mock()
+ yield mock_controller_instance
+
+
+def test_create_rocket(stub_rocket_dump, mock_controller_instance):
+ mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123'))
+ mock_controller_instance.post_rocket = mock_response
+ response = client.post('/rockets/', json=stub_rocket_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'rocket_id': '123',
+ 'message': 'Rocket successfully created',
+ }
+ mock_controller_instance.post_rocket.assert_called_once_with(
+ RocketModel(**stub_rocket_dump)
+ )
+
+
+def test_create_rocket_optional_params(
+ stub_rocket_dump,
+ stub_tail_dump,
+ stub_rail_buttons_dump,
+ stub_parachute_dump,
+ mock_controller_instance,
+):
+ stub_rocket_dump.update(
+ {
+ 'parachutes': [stub_parachute_dump],
+ 'rail_buttons': stub_rail_buttons_dump,
+ 'tail': stub_tail_dump,
+ }
+ )
+ mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123'))
+ mock_controller_instance.post_rocket = mock_response
+ response = client.post('/rockets/', json=stub_rocket_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'rocket_id': '123',
+ 'message': 'Rocket successfully created',
+ }
+ mock_controller_instance.post_rocket.assert_called_once_with(
+ RocketModel(**stub_rocket_dump)
+ )
+
+
+def test_create_generic_motor_rocket(
+ stub_rocket_dump, stub_motor_dump, mock_controller_instance
+):
+ stub_motor_dump.update(
+ {
+ 'chamber_radius': 0,
+ 'chamber_height': 0,
+ 'chamber_position': 0,
+ 'propellant_initial_mass': 0,
+ 'nozzle_position': 0,
+ 'motor_kind': 'GENERIC',
+ }
+ )
+ stub_rocket_dump.update({'motor': stub_motor_dump})
+ mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123'))
+ mock_controller_instance.post_rocket = mock_response
+ response = client.post('/rockets/', json=stub_rocket_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'rocket_id': '123',
+ 'message': 'Rocket successfully created',
+ }
+ mock_controller_instance.post_rocket.assert_called_once_with(
+ RocketModel(**stub_rocket_dump)
+ )
+
+
+def test_create_liquid_motor_level_tank_rocket(
+ stub_rocket_dump,
+ stub_motor_dump,
+ stub_level_tank_dump,
+ mock_controller_instance,
+):
+ stub_motor_dump.update(
+ {'tanks': [stub_level_tank_dump], 'motor_kind': 'LIQUID'}
+ )
+ stub_rocket_dump.update({'motor': stub_motor_dump})
+ mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123'))
+ mock_controller_instance.post_rocket = mock_response
+ response = client.post('/rockets/', json=stub_rocket_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'rocket_id': '123',
+ 'message': 'Rocket successfully created',
+ }
+ mock_controller_instance.post_rocket.assert_called_once_with(
+ RocketModel(**stub_rocket_dump)
+ )
+
+
+def test_create_liquid_motor_mass_flow_tank_rocket(
+ stub_rocket_dump,
+ stub_motor_dump,
+ stub_mass_flow_tank_dump,
+ mock_controller_instance,
+):
+ stub_motor_dump.update(
+ {'tanks': [stub_mass_flow_tank_dump], 'motor_kind': 'LIQUID'}
+ )
+ stub_rocket_dump.update({'motor': stub_motor_dump})
+ mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123'))
+ mock_controller_instance.post_rocket = mock_response
+ response = client.post('/rockets/', json=stub_rocket_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'rocket_id': '123',
+ 'message': 'Rocket successfully created',
+ }
+ mock_controller_instance.post_rocket.assert_called_once_with(
+ RocketModel(**stub_rocket_dump)
+ )
+
+
+def test_create_liquid_motor_ullage_tank_rocket(
+ stub_rocket_dump,
+ stub_motor_dump,
+ stub_ullage_tank_dump,
+ mock_controller_instance,
+):
+ stub_motor_dump.update(
+ {'tanks': [stub_ullage_tank_dump], 'motor_kind': 'LIQUID'}
+ )
+ stub_rocket_dump.update({'motor': stub_motor_dump})
+ mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123'))
+ mock_controller_instance.post_rocket = mock_response
+ response = client.post('/rockets/', json=stub_rocket_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'rocket_id': '123',
+ 'message': 'Rocket successfully created',
+ }
+ mock_controller_instance.post_rocket.assert_called_once_with(
+ RocketModel(**stub_rocket_dump)
+ )
+
+
+def test_create_liquid_motor_mass_tank_rocket(
+ stub_rocket_dump,
+ stub_motor_dump,
+ stub_mass_tank_dump,
+ mock_controller_instance,
+):
+ stub_motor_dump.update(
+ {'tanks': [stub_mass_tank_dump], 'motor_kind': 'LIQUID'}
+ )
+ stub_rocket_dump.update({'motor': stub_motor_dump})
+ mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123'))
+ mock_controller_instance.post_rocket = mock_response
+ response = client.post('/rockets/', json=stub_rocket_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'rocket_id': '123',
+ 'message': 'Rocket successfully created',
+ }
+ mock_controller_instance.post_rocket.assert_called_once_with(
+ RocketModel(**stub_rocket_dump)
+ )
+
+
+def test_create_hybrid_motor_rocket(
+ stub_rocket_dump,
+ stub_motor_dump,
+ stub_level_tank_dump,
+ mock_controller_instance,
+):
+ stub_motor_dump.update(
+ {
+ 'grain_number': 0,
+ 'grain_density': 0,
+ 'grain_outer_radius': 0,
+ 'grain_initial_inner_radius': 0,
+ 'grain_initial_height': 0,
+ 'grains_center_of_mass_position': 0,
+ 'grain_separation': 0,
+ 'throat_radius': 0,
+ 'motor_kind': 'HYBRID',
+ 'tanks': [stub_level_tank_dump],
+ }
+ )
+ stub_rocket_dump.update({'motor': stub_motor_dump})
+ mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123'))
+ mock_controller_instance.post_rocket = mock_response
+ response = client.post('/rockets/', json=stub_rocket_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'rocket_id': '123',
+ 'message': 'Rocket successfully created',
+ }
+ mock_controller_instance.post_rocket.assert_called_once_with(
+ RocketModel(**stub_rocket_dump)
+ )
+
+
+def test_create_solid_motor_rocket(
+ stub_rocket_dump, stub_motor_dump, mock_controller_instance
+):
+ stub_motor_dump.update(
+ {
+ 'grain_number': 0,
+ 'grain_density': 0,
+ 'grain_outer_radius': 0,
+ 'grain_initial_inner_radius': 0,
+ 'grain_initial_height': 0,
+ 'grains_center_of_mass_position': 0,
+ 'grain_separation': 0,
+ 'motor_kind': 'SOLID',
+ }
+ )
+ stub_rocket_dump.update({'motor': stub_motor_dump})
+ mock_response = AsyncMock(return_value=RocketCreated(rocket_id='123'))
+ mock_controller_instance.post_rocket = mock_response
+ response = client.post('/rockets/', json=stub_rocket_dump)
+ assert response.status_code == 201
+ assert response.json() == {
+ 'rocket_id': '123',
+ 'message': 'Rocket successfully created',
+ }
+ mock_controller_instance.post_rocket.assert_called_once_with(
+ RocketModel(**stub_rocket_dump)
+ )
+
+
+def test_create_rocket_invalid_input():
+ response = client.post('/rockets/', json={'radius': 'foo', 'mass': 'bar'})
+ assert response.status_code == 422
+
+
+def test_create_rocket_server_error(
+ stub_rocket_dump, mock_controller_instance
+):
+ mock_controller_instance.post_rocket.side_effect = HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+ response = client.post('/rockets/', json=stub_rocket_dump)
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_read_rocket(
+ stub_rocket_dump, stub_motor_dump, mock_controller_instance
+):
+ stub_rocket_dump.update({'motor': stub_motor_dump})
+ rocket_view = RocketView(rocket_id='123', **stub_rocket_dump)
+ mock_response = AsyncMock(return_value=RocketRetrieved(rocket=rocket_view))
+ mock_controller_instance.get_rocket_by_id = mock_response
+ response = client.get('/rockets/123')
+ assert response.status_code == 200
+ assert response.json() == {
+ 'message': 'Rocket successfully retrieved',
+ 'rocket': json.loads(rocket_view.model_dump_json()),
+ }
+ mock_controller_instance.get_rocket_by_id.assert_called_once_with('123')
+
+
+def test_read_rocket_not_found(mock_controller_instance):
+ mock_controller_instance.get_rocket_by_id.side_effect = HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND
+ )
+ response = client.get('/rockets/123')
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+
+
+def test_read_rocket_server_error(mock_controller_instance):
+ mock_controller_instance.get_rocket_by_id.side_effect = HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+ response = client.get('/rockets/123')
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_update_rocket(stub_rocket_dump, mock_controller_instance):
+ stub_rocket_dump['motor']['motor_kind'] = 'SOLID'
+ mock_response = AsyncMock(return_value=None)
+ mock_controller_instance.put_rocket_by_id = mock_response
+ response = client.put('/rockets/123', json=stub_rocket_dump)
+ assert response.status_code == 204
+ mock_controller_instance.put_rocket_by_id.assert_called_once_with(
+ '123', RocketModel(**stub_rocket_dump)
+ )
+
+
+def test_update_rocket_invalid_input():
+ response = client.put(
+ '/rockets/123', json={'mass': 'foo', 'radius': 'bar'}
+ )
+ assert response.status_code == 422
+
+
+def test_update_rocket_not_found(stub_rocket_dump, mock_controller_instance):
+ mock_controller_instance.put_rocket_by_id.side_effect = HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND
+ )
+ response = client.put('/rockets/123', json=stub_rocket_dump)
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+
+
+def test_update_rocket_server_error(
+ stub_rocket_dump, mock_controller_instance
+):
+ mock_controller_instance.put_rocket_by_id.side_effect = HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+ response = client.put('/rockets/123', json=stub_rocket_dump)
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_delete_rocket(mock_controller_instance):
+ mock_response = AsyncMock(return_value=None)
+ mock_controller_instance.delete_rocket_by_id = mock_response
+ response = client.delete('/rockets/123')
+ assert response.status_code == 204
+ mock_controller_instance.delete_rocket_by_id.assert_called_once_with('123')
+
+
+def test_delete_rocket_server_error(mock_controller_instance):
+ mock_controller_instance.delete_rocket_by_id.side_effect = HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+ response = client.delete('/rockets/123')
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_get_rocket_simulation(
+ stub_rocket_simulation_dump, mock_controller_instance
+):
+ mock_response = AsyncMock(
+ return_value=RocketSimulation(**stub_rocket_simulation_dump)
+ )
+ mock_controller_instance.get_rocket_simulation = mock_response
+ response = client.get('/rockets/123/simulate')
+ assert response.status_code == 200
+ assert response.json() == stub_rocket_simulation_dump
+ mock_controller_instance.get_rocket_simulation.assert_called_once_with(
+ '123'
+ )
+
+
+def test_get_rocket_simulation_not_found(mock_controller_instance):
+ mock_controller_instance.get_rocket_simulation.side_effect = HTTPException(
+ status_code=status.HTTP_404_NOT_FOUND
+ )
+ response = client.get('/rockets/123/simulate')
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+
+
+def test_get_rocket_simulation_server_error(mock_controller_instance):
+ mock_controller_instance.get_rocket_simulation.side_effect = HTTPException(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR
+ )
+ response = client.get('/rockets/123/simulate')
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}
+
+
+def test_read_rocketpy_rocket_binary(mock_controller_instance):
+ mock_response = AsyncMock(return_value=b'rocketpy')
+ mock_controller_instance.get_rocketpy_rocket_binary = mock_response
+ response = client.get('/rockets/123/rocketpy')
+ assert response.status_code == 200
+ assert response.content == b'rocketpy'
+ assert response.headers['content-type'] == 'application/octet-stream'
+ mock_controller_instance.get_rocketpy_rocket_binary.assert_called_once_with(
+ '123'
+ )
+
+
+def test_read_rocketpy_rocket_binary_not_found(mock_controller_instance):
+ mock_controller_instance.get_rocketpy_rocket_binary.side_effect = (
+ HTTPException(status_code=status.HTTP_404_NOT_FOUND)
+ )
+ response = client.get('/rockets/123/rocketpy')
+ assert response.status_code == 404
+ assert response.json() == {'detail': 'Not Found'}
+
+
+def test_read_rocketpy_rocket_binary_server_error(mock_controller_instance):
+ mock_controller_instance.get_rocketpy_rocket_binary.side_effect = (
+ HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR)
+ )
+ response = client.get('/rockets/123/rocketpy')
+ assert response.status_code == 500
+ assert response.json() == {'detail': 'Internal Server Error'}