Skip to content
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 62 additions & 0 deletions src/dependencies.py
Original file line number Diff line number Diff line change
@@ -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)]
29 changes: 18 additions & 11 deletions src/routes/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -29,6 +29,7 @@
@router.post("/", status_code=201)
async def create_environment(
environment: EnvironmentModel,
controller: EnvironmentControllerDep,
) -> EnvironmentCreated:
"""
Creates a new environment
Expand All @@ -37,26 +38,29 @@ 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

## 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
environment_id: str,
environment: EnvironmentModel,
controller: EnvironmentControllerDep,
) -> None:
"""
Updates an existing environment
Expand All @@ -68,22 +72,23 @@ 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

## Args
``` environment_id: str ```
"""
with tracer.start_as_current_span("delete_environment"):
controller = EnvironmentController()
return await controller.delete_environment_by_id(environment_id)


Expand All @@ -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.
Expand All @@ -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
)
Expand All @@ -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
Expand All @@ -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)
67 changes: 42 additions & 25 deletions src/routes/flight.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Flight routes
Flight routes with dependency injection for improved performance.
"""

from fastapi import APIRouter, Response
Expand All @@ -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",
Expand All @@ -29,21 +29,24 @@


@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

## Args
``` 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.
Expand All @@ -56,25 +59,29 @@ 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

## Args
``` flight_id: str ```
"""
with tracer.start_as_current_span("read_flight"):
controller = FlightController()
return await controller.get_flight_by_id(flight_id)



Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent spacing: this line uses 4 spaces for indentation instead of the blank line pattern used elsewhere. For consistency with the rest of the file, consider using a blank line without trailing spaces here.

Suggested change

Copilot uses AI. Check for mistakes.
@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

Expand All @@ -85,14 +92,14 @@ 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)


@router.put("/{flight_id}/from-references", status_code=204)
async def update_flight_from_references(
flight_id: str,
payload: FlightWithReferencesRequest,
controller: FlightControllerDep,
) -> None:
"""
Updates a flight using existing rocket and environment references.
Expand All @@ -106,22 +113,22 @@ 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
)



Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent spacing: this line uses 4 spaces for indentation instead of the blank line pattern used elsewhere. For consistency with the rest of the file, consider using a blank line without trailing spaces here.

Suggested change

Copilot uses AI. Check for mistakes.
@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

## Args
``` flight_id: str ```
"""
with tracer.start_as_current_span("delete_flight"):
controller = FlightController()
return await controller.delete_flight_by_id(flight_id)


Expand All @@ -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.
Expand All @@ -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"'
}
Expand All @@ -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
Expand All @@ -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.

Expand All @@ -190,21 +205,23 @@ 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,
)


Comment on lines +212 to 213
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent spacing: this line uses 4 spaces for indentation instead of the blank line pattern used elsewhere. For consistency with the rest of the file, consider using a blank line without trailing spaces here.

Suggested change

Copilot uses AI. Check for mistakes.
@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

## Args
``` flight_id: Flight ID ```
"""
with tracer.start_as_current_span("get_flight_simulation"):
controller = FlightController()
return await controller.get_flight_simulation(flight_id)

Comment on lines 226 to +227
Copy link

Copilot AI Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent spacing: this line uses 4 spaces for indentation instead of the blank line pattern used elsewhere. For consistency with the rest of the file, consider using a blank line without trailing spaces here.

Suggested change
return await controller.get_flight_simulation(flight_id)
return await controller.get_flight_simulation(flight_id)

Copilot uses AI. Check for mistakes.
Loading