From 897afee60d6271815bea3c1b3cd2c38201f51b49 Mon Sep 17 00:00:00 2001 From: Marcos Date: Sun, 7 Dec 2025 16:23:15 -0300 Subject: [PATCH 01/11] ENH: Add dependency injection for controller singletons - Create FastAPI dependency providers for all controllers - Implement singleton pattern using lru_cache for thread-safety - Add type aliases (RocketControllerDep, MotorControllerDep, etc.) - Improves performance by avoiding controller re-instantiation on every request References: - FastAPI Dependencies: https://fastapi.tiangolo.com/tutorial/dependencies/ --- src/dependencies.py | 62 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 src/dependencies.py diff --git a/src/dependencies.py b/src/dependencies.py new file mode 100644 index 0000000..dcb410a --- /dev/null +++ b/src/dependencies.py @@ -0,0 +1,62 @@ +from functools import lru_cache +from typing import Annotated + +from fastapi import Depends + +from src.controllers.rocket import RocketController +from src.controllers.motor import MotorController +from src.controllers.environment import EnvironmentController +from src.controllers.flight import FlightController + +@lru_cache(maxsize=1) +def get_rocket_controller() -> RocketController: + """ + Provides a singleton RocketController instance. + + The controller is stateless and can be safely reused across requests. + Using lru_cache ensures thread-safe singleton behavior. + + Returns: + RocketController: Shared controller instance for rocket operations. + """ + return RocketController() + + +@lru_cache(maxsize=1) +def get_motor_controller() -> MotorController: + """ + Provides a singleton MotorController instance. + + Returns: + MotorController: Shared controller instance for motor operations. + """ + return MotorController() + + +@lru_cache(maxsize=1) +def get_environment_controller() -> EnvironmentController: + """ + Provides a singleton EnvironmentController instance. + + Returns: + EnvironmentController: Shared controller instance for environment operations. + """ + return EnvironmentController() + + +@lru_cache(maxsize=1) +def get_flight_controller() -> FlightController: + """ + Provides a singleton FlightController instance. + + Returns: + FlightController: Shared controller instance for flight operations. + """ + return FlightController() + +RocketControllerDep = Annotated[RocketController, Depends(get_rocket_controller)] +MotorControllerDep = Annotated[MotorController, Depends(get_motor_controller)] +EnvironmentControllerDep = Annotated[ + EnvironmentController, Depends(get_environment_controller) +] +FlightControllerDep = Annotated[FlightController, Depends(get_flight_controller)] From 81d64f58b4066151424c7b3821f6ff730618ab1a Mon Sep 17 00:00:00 2001 From: Marcos Date: Sun, 7 Dec 2025 16:23:57 -0300 Subject: [PATCH 02/11] ENH: Refactor flight routes to use dependency injection - Replace manual FlightController instantiation with FlightControllerDep - Apply DI pattern across all flight endpoints (CRUD + simulate + binary) - Maintains backward compatibility with existing API contracts --- src/routes/flight.py | 67 +++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/src/routes/flight.py b/src/routes/flight.py index 6afd13a..73a2c01 100644 --- a/src/routes/flight.py +++ b/src/routes/flight.py @@ -1,5 +1,5 @@ """ -Flight routes +Flight routes with dependency injection for improved performance. """ from fastapi import APIRouter, Response @@ -13,7 +13,7 @@ from src.models.environment import EnvironmentModel from src.models.flight import FlightModel, FlightWithReferencesRequest from src.models.rocket import RocketModel -from src.controllers.flight import FlightController +from src.dependencies import FlightControllerDep router = APIRouter( prefix="/flights", @@ -29,7 +29,10 @@ @router.post("/", status_code=201) -async def create_flight(flight: FlightModel) -> FlightCreated: +async def create_flight( + flight: FlightModel, + controller: FlightControllerDep, +) -> FlightCreated: """ Creates a new flight @@ -37,13 +40,13 @@ async def create_flight(flight: FlightModel) -> FlightCreated: ``` models.Flight JSON ``` """ with tracer.start_as_current_span("create_flight"): - controller = FlightController() return await controller.post_flight(flight) @router.post("/from-references", status_code=201) async def create_flight_from_references( payload: FlightWithReferencesRequest, + controller: FlightControllerDep, ) -> FlightCreated: """ Creates a flight using existing rocket and environment references. @@ -56,12 +59,14 @@ async def create_flight_from_references( ``` """ with tracer.start_as_current_span("create_flight_from_references"): - controller = FlightController() return await controller.create_flight_from_references(payload) @router.get("/{flight_id}") -async def read_flight(flight_id: str) -> FlightRetrieved: +async def read_flight( + flight_id: str, + controller: FlightControllerDep, +) -> FlightRetrieved: """ Reads an existing flight @@ -69,12 +74,14 @@ async def read_flight(flight_id: str) -> FlightRetrieved: ``` 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: +async def update_flight( + flight_id: str, + flight: FlightModel, + controller: FlightControllerDep, +) -> None: """ Updates an existing flight @@ -85,7 +92,6 @@ async def update_flight(flight_id: str, flight: FlightModel) -> None: ``` """ with tracer.start_as_current_span("update_flight"): - controller = FlightController() return await controller.put_flight_by_id(flight_id, flight) @@ -93,6 +99,7 @@ async def update_flight(flight_id: str, flight: FlightModel) -> None: async def update_flight_from_references( flight_id: str, payload: FlightWithReferencesRequest, + controller: FlightControllerDep, ) -> None: """ Updates a flight using existing rocket and environment references. @@ -106,14 +113,15 @@ async def update_flight_from_references( ``` """ with tracer.start_as_current_span("update_flight_from_references"): - controller = FlightController() return await controller.update_flight_from_references( flight_id, payload ) - - + @router.delete("/{flight_id}", status_code=204) -async def delete_flight(flight_id: str) -> None: +async def delete_flight( + flight_id: str, + controller: FlightControllerDep, +) -> None: """ Deletes an existing flight @@ -121,7 +129,6 @@ async def delete_flight(flight_id: str) -> None: ``` flight_id: str ``` """ with tracer.start_as_current_span("delete_flight"): - controller = FlightController() return await controller.delete_flight_by_id(flight_id) @@ -136,7 +143,11 @@ async def delete_flight(flight_id: str) -> None: status_code=200, response_class=Response, ) -async def get_rocketpy_flight_binary(flight_id: str): + +async def get_rocketpy_flight_binary( + flight_id: str, + controller: FlightControllerDep, +): """ Loads rocketpy.flight as a dill binary. Currently only amd64 architecture is supported. @@ -145,7 +156,6 @@ async def get_rocketpy_flight_binary(flight_id: str): ``` 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"' } @@ -160,7 +170,9 @@ async def get_rocketpy_flight_binary(flight_id: str): @router.put("/{flight_id}/environment", status_code=204) async def update_flight_environment( - flight_id: str, environment: EnvironmentModel + flight_id: str, + environment: EnvironmentModel, + controller: FlightControllerDep, ) -> None: """ Updates flight environment @@ -172,14 +184,17 @@ async def update_flight_environment( ``` """ 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: +async def update_flight_rocket( + flight_id: str, + rocket: RocketModel, + controller: FlightControllerDep, +) -> None: """ Updates flight rocket. @@ -190,15 +205,17 @@ async def update_flight_rocket(flight_id: str, rocket: RocketModel) -> None: ``` """ 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: +async def get_flight_simulation( + flight_id: str, + controller: FlightControllerDep, +) -> FlightSimulation: """ Simulates a flight @@ -206,5 +223,5 @@ async def get_flight_simulation(flight_id: str) -> FlightSimulation: ``` flight_id: Flight ID ``` """ with tracer.start_as_current_span("get_flight_simulation"): - controller = FlightController() return await controller.get_flight_simulation(flight_id) + \ No newline at end of file From a85b9cb74c9c5b90d055ba5b26126a18cb580362 Mon Sep 17 00:00:00 2001 From: Marcos Date: Sun, 7 Dec 2025 16:25:12 -0300 Subject: [PATCH 03/11] ENH: Refactor rocket routes to use dependency injection - Replace manual RocketController instantiation with RocketControllerDep - Apply DI pattern to all rocket endpoints including motor-reference flows --- src/routes/rocket.py | 46 +++++++++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/src/routes/rocket.py b/src/routes/rocket.py index d5e4bdf..860f3a9 100644 --- a/src/routes/rocket.py +++ b/src/routes/rocket.py @@ -14,7 +14,7 @@ RocketModel, RocketWithMotorReferenceRequest, ) -from src.controllers.rocket import RocketController +from src.dependencies import RocketControllerDep router = APIRouter( prefix="/rockets", @@ -30,7 +30,10 @@ @router.post("/", status_code=201) -async def create_rocket(rocket: RocketModel) -> RocketCreated: +async def create_rocket( + rocket: RocketModel, + controller: RocketControllerDep, +) -> RocketCreated: """ Creates a new rocket @@ -38,13 +41,11 @@ async def create_rocket(rocket: RocketModel) -> RocketCreated: ``` models.Rocket JSON ``` """ with tracer.start_as_current_span("create_rocket"): - controller = RocketController() return await controller.post_rocket(rocket) - - @router.post("/from-motor-reference", status_code=201) async def create_rocket_from_motor_reference( payload: RocketWithMotorReferenceRequest, + controller: RocketControllerDep, ) -> RocketCreated: """ Creates a rocket using an existing motor reference. @@ -56,12 +57,14 @@ async def create_rocket_from_motor_reference( ``` """ with tracer.start_as_current_span("create_rocket_from_motor_reference"): - controller = RocketController() return await controller.create_rocket_from_motor_reference(payload) @router.get("/{rocket_id}") -async def read_rocket(rocket_id: str) -> RocketRetrieved: +async def read_rocket( + rocket_id: str, + controller: RocketControllerDep, +) -> RocketRetrieved: """ Reads an existing rocket @@ -69,12 +72,15 @@ async def read_rocket(rocket_id: str) -> RocketRetrieved: ``` 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: +async def update_rocket( + rocket_id: str, + rocket: RocketModel, + controller: RocketControllerDep, +) -> None: """ Updates an existing rocket @@ -85,7 +91,6 @@ async def update_rocket(rocket_id: str, rocket: RocketModel) -> None: ``` """ with tracer.start_as_current_span("update_rocket"): - controller = RocketController() return await controller.put_rocket_by_id(rocket_id, rocket) @@ -93,6 +98,7 @@ async def update_rocket(rocket_id: str, rocket: RocketModel) -> None: async def update_rocket_from_motor_reference( rocket_id: str, payload: RocketWithMotorReferenceRequest, + controller: RocketControllerDep, ) -> None: """ Updates a rocket using an existing motor reference. @@ -105,14 +111,15 @@ async def update_rocket_from_motor_reference( ``` """ with tracer.start_as_current_span("update_rocket_from_motor_reference"): - controller = RocketController() return await controller.update_rocket_from_motor_reference( rocket_id, payload ) - @router.delete("/{rocket_id}", status_code=204) -async def delete_rocket(rocket_id: str) -> None: +async def delete_rocket( + rocket_id: str, + controller: RocketControllerDep, +) -> None: """ Deletes an existing rocket @@ -120,7 +127,6 @@ async def delete_rocket(rocket_id: str) -> None: ``` rocket_id: str ``` """ with tracer.start_as_current_span("delete_rocket"): - controller = RocketController() return await controller.delete_rocket_by_id(rocket_id) @@ -135,7 +141,10 @@ async def delete_rocket(rocket_id: str) -> None: status_code=200, response_class=Response, ) -async def get_rocketpy_rocket_binary(rocket_id: str): +async def get_rocketpy_rocket_binary( + rocket_id: str, + controller: RocketControllerDep, +): """ Loads rocketpy.rocket as a dill binary. Currently only amd64 architecture is supported. @@ -147,7 +156,6 @@ async def get_rocketpy_rocket_binary(rocket_id: str): 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, @@ -158,7 +166,10 @@ async def get_rocketpy_rocket_binary(rocket_id: str): @router.get("/{rocket_id}/simulate") -async def simulate_rocket(rocket_id: str) -> RocketSimulation: +async def simulate_rocket( + rocket_id: str, + controller: RocketControllerDep, +) -> RocketSimulation: """ Simulates a rocket @@ -166,5 +177,4 @@ async def simulate_rocket(rocket_id: str) -> RocketSimulation: ``` rocket_id: Rocket ID ``` """ with tracer.start_as_current_span("get_rocket_simulation"): - controller = RocketController() return await controller.get_rocket_simulation(rocket_id) From a75f8d2d597249ebbc4645701ca1453ca478bd55 Mon Sep 17 00:00:00 2001 From: Marcos Date: Sun, 7 Dec 2025 16:26:48 -0300 Subject: [PATCH 04/11] ENH: Refactor motor routes to use dependency injection - Replace manual MotorController instantiation with MotorControllerDep - Apply DI pattern to all motor endpoints (CRUD + simulate + binary) --- src/routes/motor.py | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/routes/motor.py b/src/routes/motor.py index 3143c26..0673708 100644 --- a/src/routes/motor.py +++ b/src/routes/motor.py @@ -11,7 +11,7 @@ MotorRetrieved, ) from src.models.motor import MotorModel -from src.controllers.motor import MotorController +from src.dependencies import MotorControllerDep router = APIRouter( prefix="/motors", @@ -27,7 +27,10 @@ @router.post("/", status_code=201) -async def create_motor(motor: MotorModel) -> MotorCreated: +async def create_motor( + motor: MotorModel, + controller: MotorControllerDep, +) -> MotorCreated: """ Creates a new motor @@ -35,12 +38,14 @@ async def create_motor(motor: MotorModel) -> MotorCreated: ``` 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: +async def read_motor( + motor_id: str, + controller: MotorControllerDep, +) -> MotorRetrieved: """ Reads an existing motor @@ -48,12 +53,15 @@ async def read_motor(motor_id: str) -> MotorRetrieved: ``` 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: +async def update_motor( + motor_id: str, + motor: MotorModel, + controller: MotorControllerDep, +) -> None: """ Updates an existing motor @@ -64,12 +72,14 @@ async def update_motor(motor_id: str, motor: MotorModel) -> None: ``` """ 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: +async def delete_motor( + motor_id: str, + controller: MotorControllerDep, +) -> None: """ Deletes an existing motor @@ -77,7 +87,6 @@ async def delete_motor(motor_id: str) -> None: ``` motor_id: str ``` """ with tracer.start_as_current_span("delete_motor"): - controller = MotorController() return await controller.delete_motor_by_id(motor_id) @@ -92,7 +101,10 @@ async def delete_motor(motor_id: str) -> None: status_code=200, response_class=Response, ) -async def get_rocketpy_motor_binary(motor_id: str): +async def get_rocketpy_motor_binary( + motor_id: str, + controller: MotorControllerDep, +): """ Loads rocketpy.motor as a dill binary. Currently only amd64 architecture is supported. @@ -104,7 +116,6 @@ async def get_rocketpy_motor_binary(motor_id: str): 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, @@ -115,7 +126,10 @@ async def get_rocketpy_motor_binary(motor_id: str): @router.get("/{motor_id}/simulate") -async def get_motor_simulation(motor_id: str) -> MotorSimulation: +async def get_motor_simulation( + motor_id: str, + controller: MotorControllerDep, +) -> MotorSimulation: """ Simulates a motor @@ -123,5 +137,4 @@ async def get_motor_simulation(motor_id: str) -> MotorSimulation: ``` motor_id: Motor ID ``` """ with tracer.start_as_current_span("get_motor_simulation"): - controller = MotorController() return await controller.get_motor_simulation(motor_id) From 9a7f0d74a1d189f7017048c5ccd2f9494adb9ade Mon Sep 17 00:00:00 2001 From: Marcos Date: Sun, 7 Dec 2025 16:27:28 -0300 Subject: [PATCH 05/11] ENH: Refactor environment routes to use dependency injection - Replace manual EnvironmentController instantiation with EnvironmentControllerDep - Complete DI migration across all API routes - All controllers now use singleton pattern for optimal performance --- src/routes/environment.py | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/routes/environment.py b/src/routes/environment.py index 6c5e8b2..3879c50 100644 --- a/src/routes/environment.py +++ b/src/routes/environment.py @@ -11,7 +11,7 @@ EnvironmentRetrieved, ) from src.models.environment import EnvironmentModel -from src.controllers.environment import EnvironmentController +from src.dependencies import EnvironmentControllerDep router = APIRouter( prefix="/environments", @@ -29,6 +29,7 @@ @router.post("/", status_code=201) async def create_environment( environment: EnvironmentModel, + controller: EnvironmentControllerDep, ) -> EnvironmentCreated: """ Creates a new environment @@ -37,12 +38,14 @@ async def create_environment( ``` 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: +async def read_environment( + environment_id: str, + controller: EnvironmentControllerDep, +) -> EnvironmentRetrieved: """ Reads an existing environment @@ -50,13 +53,14 @@ async def read_environment(environment_id: str) -> EnvironmentRetrieved: ``` 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 + environment_id: str, + environment: EnvironmentModel, + controller: EnvironmentControllerDep, ) -> None: """ Updates an existing environment @@ -68,14 +72,16 @@ async def update_environment( ``` """ 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: +async def delete_environment( + environment_id: str, + controller: EnvironmentControllerDep, +) -> None: """ Deletes an existing environment @@ -83,7 +89,6 @@ async def delete_environment(environment_id: str) -> None: ``` environment_id: str ``` """ with tracer.start_as_current_span("delete_environment"): - controller = EnvironmentController() return await controller.delete_environment_by_id(environment_id) @@ -98,7 +103,10 @@ async def delete_environment(environment_id: str) -> None: status_code=200, response_class=Response, ) -async def get_rocketpy_environment_binary(environment_id: str): +async def get_rocketpy_environment_binary( + environment_id: str, + controller: EnvironmentControllerDep, +): """ Loads rocketpy.environment as a dill binary. Currently only amd64 architecture is supported. @@ -110,7 +118,6 @@ async def get_rocketpy_environment_binary(environment_id: str): headers = { 'Content-Disposition': f'attachment; filename="rocketpy_environment_{environment_id}.dill"' } - controller = EnvironmentController() binary = await controller.get_rocketpy_environment_binary( environment_id ) @@ -125,6 +132,7 @@ async def get_rocketpy_environment_binary(environment_id: str): @router.get("/{environment_id}/simulate") async def get_environment_simulation( environment_id: str, + controller: EnvironmentControllerDep, ) -> EnvironmentSimulation: """ Simulates an environment @@ -133,5 +141,4 @@ async def get_environment_simulation( ``` environment_id: Environment ID``` """ with tracer.start_as_current_span("get_environment_simulation"): - controller = EnvironmentController() return await controller.get_environment_simulation(environment_id) From b6f27b4f154ce5a818e3402df4ca03c7e14c49e0 Mon Sep 17 00:00:00 2001 From: Marcos Date: Sun, 7 Dec 2025 19:56:24 -0300 Subject: [PATCH 06/11] ENH: update route tests to use dependency injection with cache clearing - Replace patch from src.routes to src.dependencies for controller mocking - Use AsyncMock for all controller methods (they are async functions) - Clear lru_cache before and after each test to ensure fresh mock instances --- .../test_routes/test_environments_route.py | 31 +++++++++------ tests/unit/test_routes/test_flights_route.py | 38 +++++++++++-------- tests/unit/test_routes/test_motors_route.py | 32 ++++++++++------ tests/unit/test_routes/test_rockets_route.py | 35 ++++++++++------- 4 files changed, 85 insertions(+), 51 deletions(-) diff --git a/tests/unit/test_routes/test_environments_route.py b/tests/unit/test_routes/test_environments_route.py index 93ff4c3..5e8adb5 100644 --- a/tests/unit/test_routes/test_environments_route.py +++ b/tests/unit/test_routes/test_environments_route.py @@ -10,6 +10,9 @@ EnvironmentRetrieved, EnvironmentSimulation, ) + +from src.dependencies import get_environment_controller + from src import app client = TestClient(app) @@ -24,17 +27,23 @@ def stub_environment_simulation_dump(): @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 + with patch("src.dependencies.EnvironmentController") as mock_class: + mock_controller = AsyncMock() + mock_controller.post_environment = AsyncMock() + mock_controller.get_environment_by_id = AsyncMock() + mock_controller.put_environment_by_id = AsyncMock() + mock_controller.delete_environment_by_id = AsyncMock() + mock_controller.get_environment_simulation = AsyncMock() + mock_controller.get_rocketpy_environment_binary = AsyncMock() + + mock_class.return_value = mock_controller + + + get_environment_controller.cache_clear() + + yield mock_controller + + get_environment_controller.cache_clear() def test_create_environment(stub_environment_dump, mock_controller_instance): diff --git a/tests/unit/test_routes/test_flights_route.py b/tests/unit/test_routes/test_flights_route.py index 5190c45..d53ca54 100644 --- a/tests/unit/test_routes/test_flights_route.py +++ b/tests/unit/test_routes/test_flights_route.py @@ -15,6 +15,9 @@ FlightSimulation, FlightView, ) + +from src.dependencies import get_flight_controller + from src import app client = TestClient(app) @@ -43,21 +46,26 @@ def stub_flight_simulate_dump(): @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() - mock_controller_instance.create_flight_from_references = Mock() - mock_controller_instance.update_flight_from_references = Mock() - yield mock_controller_instance + with patch("src.dependencies.FlightController") as mock_class: + mock_controller = AsyncMock() + mock_controller.post_flight = AsyncMock() + mock_controller.get_flight_by_id = AsyncMock() + mock_controller.put_flight_by_id = AsyncMock() + mock_controller.delete_flight_by_id = AsyncMock() + mock_controller.get_flight_simulation = AsyncMock() + mock_controller.get_rocketpy_flight_binary = AsyncMock() + mock_controller.update_environment_by_flight_id = AsyncMock() + mock_controller.update_rocket_by_flight_id = AsyncMock() + mock_controller.create_flight_from_references = AsyncMock() + mock_controller.update_flight_from_references = AsyncMock() + + mock_class.return_value = mock_controller + + get_flight_controller.cache_clear() + + yield mock_controller + + get_flight_controller.cache_clear() @pytest.fixture diff --git a/tests/unit/test_routes/test_motors_route.py b/tests/unit/test_routes/test_motors_route.py index e55c976..2ad7fc2 100644 --- a/tests/unit/test_routes/test_motors_route.py +++ b/tests/unit/test_routes/test_motors_route.py @@ -9,7 +9,10 @@ MotorRetrieved, MotorSimulation, MotorView, -) +) + +from src.dependencies import get_motor_controller + from src import app client = TestClient(app) @@ -24,17 +27,22 @@ def stub_motor_dump_simulation(): @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 + with patch("src.dependencies.MotorController") as mock_class: + mock_controller = AsyncMock() + mock_controller.post_motor = AsyncMock() + mock_controller.get_motor_by_id = AsyncMock() + mock_controller.put_motor_by_id = AsyncMock() + mock_controller.delete_motor_by_id = AsyncMock() + mock_controller.get_motor_simulation = AsyncMock() + mock_controller.get_rocketpy_motor_binary = AsyncMock() + + mock_class.return_value = mock_controller + + get_motor_controller.cache_clear() + + yield mock_controller + + get_motor_controller.cache_clear() def test_create_motor(stub_motor_dump, mock_controller_instance): diff --git a/tests/unit/test_routes/test_rockets_route.py b/tests/unit/test_routes/test_rockets_route.py index a91041d..075f8ab 100644 --- a/tests/unit/test_routes/test_rockets_route.py +++ b/tests/unit/test_routes/test_rockets_route.py @@ -19,6 +19,10 @@ RocketSimulation, RocketView, ) + + +from src.dependencies import get_rocket_controller + from src import app client = TestClient(app) @@ -72,19 +76,24 @@ def stub_parachute_dump(): @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() - mock_controller_instance.create_rocket_from_motor_reference = Mock() - mock_controller_instance.update_rocket_from_motor_reference = Mock() - yield mock_controller_instance + with patch("src.dependencies.RocketController") as mock_class: + mock_controller = AsyncMock() + mock_controller.post_rocket = AsyncMock() + mock_controller.get_rocket_by_id = AsyncMock() + mock_controller.put_rocket_by_id = AsyncMock() + mock_controller.delete_rocket_by_id = AsyncMock() + mock_controller.get_rocket_simulation = AsyncMock() + mock_controller.get_rocketpy_rocket_binary = AsyncMock() + mock_controller.create_rocket_from_motor_reference = AsyncMock() + mock_controller.update_rocket_from_motor_reference = AsyncMock() + + mock_class.return_value = mock_controller + + get_rocket_controller.cache_clear() + + yield mock_controller + + get_rocket_controller.cache_clear() @pytest.fixture From 24a0dfcc922522fea9e90ecdd34c6c5c1ca43211 Mon Sep 17 00:00:00 2001 From: Marcos Date: Sun, 7 Dec 2025 20:08:07 -0300 Subject: [PATCH 07/11] DOC: add Windows uvloop compatibility notice and Docker recommendation to README --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 59d435b..db6ac78 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,14 @@ - [install mongodb-atlas](https://www.mongodb.com/try/download/community) - Install dependencies `python3 -m pip install -r requirements.txt` +### Windows Users +⚠️ **Known Issue**: `uvloop` does not support Windows and will fail during pip installation with: +``` +RuntimeError: uvloop does not support Windows at the moment +``` + +**Recommended Solution**: Use Docker (easiest approach) + ## Development - make format - make test From 7980854d72467bd11e208f5d3d95c0a9bd434ddb Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 13 Dec 2025 15:51:58 +0100 Subject: [PATCH 08/11] Apply formatting suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/routes/flight.py | 8 +++----- tests/unit/test_routes/test_environments_route.py | 4 ++-- tests/unit/test_routes/test_rockets_route.py | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/routes/flight.py b/src/routes/flight.py index 73a2c01..03f8460 100644 --- a/src/routes/flight.py +++ b/src/routes/flight.py @@ -75,7 +75,7 @@ async def read_flight( """ with tracer.start_as_current_span("read_flight"): return await controller.get_flight_by_id(flight_id) - + @router.put("/{flight_id}", status_code=204) async def update_flight( flight_id: str, @@ -116,7 +116,7 @@ async def update_flight_from_references( return await controller.update_flight_from_references( flight_id, payload ) - + @router.delete("/{flight_id}", status_code=204) async def delete_flight( flight_id: str, @@ -209,7 +209,6 @@ async def update_flight_rocket( flight_id, rocket=rocket, ) - @router.get("/{flight_id}/simulate") async def get_flight_simulation( @@ -223,5 +222,4 @@ async def get_flight_simulation( ``` flight_id: Flight ID ``` """ with tracer.start_as_current_span("get_flight_simulation"): - return await controller.get_flight_simulation(flight_id) - \ No newline at end of file + return await controller.get_flight_simulation(flight_id) \ No newline at end of file diff --git a/tests/unit/test_routes/test_environments_route.py b/tests/unit/test_routes/test_environments_route.py index 5e8adb5..f9050a8 100644 --- a/tests/unit/test_routes/test_environments_route.py +++ b/tests/unit/test_routes/test_environments_route.py @@ -37,12 +37,12 @@ def mock_controller_instance(): mock_controller.get_rocketpy_environment_binary = AsyncMock() mock_class.return_value = mock_controller - - get_environment_controller.cache_clear() yield mock_controller + yield mock_controller + get_environment_controller.cache_clear() diff --git a/tests/unit/test_routes/test_rockets_route.py b/tests/unit/test_routes/test_rockets_route.py index 075f8ab..6bf5e1d 100644 --- a/tests/unit/test_routes/test_rockets_route.py +++ b/tests/unit/test_routes/test_rockets_route.py @@ -20,7 +20,7 @@ RocketView, ) - + from src.dependencies import get_rocket_controller from src import app From c212b9f1d6fd42994c3825d21dba4ac1d4c9c5b4 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 13 Dec 2025 15:57:49 +0100 Subject: [PATCH 09/11] Update tests/unit/test_routes/test_environments_route.py Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- tests/unit/test_routes/test_environments_route.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_routes/test_environments_route.py b/tests/unit/test_routes/test_environments_route.py index f9050a8..c5090d4 100644 --- a/tests/unit/test_routes/test_environments_route.py +++ b/tests/unit/test_routes/test_environments_route.py @@ -39,11 +39,13 @@ def mock_controller_instance(): mock_class.return_value = mock_controller get_environment_controller.cache_clear() - yield mock_controller + get_environment_controller.cache_clear() yield mock_controller get_environment_controller.cache_clear() + + get_environment_controller.cache_clear() def test_create_environment(stub_environment_dump, mock_controller_instance): From 472dd2170f83af359e394e76e48975a6399ed7b2 Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 13 Dec 2025 16:06:54 +0100 Subject: [PATCH 10/11] Update src/routes/rocket.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/routes/rocket.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/rocket.py b/src/routes/rocket.py index 860f3a9..54059b9 100644 --- a/src/routes/rocket.py +++ b/src/routes/rocket.py @@ -114,7 +114,6 @@ async def update_rocket_from_motor_reference( return await controller.update_rocket_from_motor_reference( rocket_id, payload ) - @router.delete("/{rocket_id}", status_code=204) async def delete_rocket( rocket_id: str, From d8b43e360162f23e233d3170eb68995f211609fd Mon Sep 17 00:00:00 2001 From: Gabriel Barberini Date: Sat, 13 Dec 2025 16:07:37 +0100 Subject: [PATCH 11/11] Update tests/unit/test_routes/test_motors_route.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/unit/test_routes/test_motors_route.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_routes/test_motors_route.py b/tests/unit/test_routes/test_motors_route.py index 2ad7fc2..552b94b 100644 --- a/tests/unit/test_routes/test_motors_route.py +++ b/tests/unit/test_routes/test_motors_route.py @@ -9,7 +9,7 @@ MotorRetrieved, MotorSimulation, MotorView, -) +) from src.dependencies import get_motor_controller