Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Collaborator

Choose a reason for hiding this comment

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

@MarcosMulari what would you say to the idea of using simple @cache instead?

Since there are no args here maxsize=1 will act as a Noop and cache === lru_cache(maxsize=None)

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)
63 changes: 39 additions & 24 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)


@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
)


@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,21 @@ 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

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