Skip to content

Commit 9067812

Browse files
MarcosMulariGabrielBarberiniCopilotcoderabbitai[bot]
authored
[SUGGESTION] ENH: streamline controller dependency injection in routes and tests (#66)
Co-authored-by: Gabriel Barberini <gabriel.barberini@sumup.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
1 parent d4e76d8 commit 9067812

10 files changed

Lines changed: 262 additions & 117 deletions

File tree

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414
- [install mongodb-atlas](https://www.mongodb.com/try/download/community)
1515
- Install dependencies `python3 -m pip install -r requirements.txt`
1616

17+
### Windows Users
18+
⚠️ **Known Issue**: `uvloop` does not support Windows and will fail during pip installation with:
19+
```
20+
RuntimeError: uvloop does not support Windows at the moment
21+
```
22+
23+
**Recommended Solution**: Use Docker (easiest approach)
24+
1725
## Development
1826
- make format
1927
- make test

src/dependencies.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
from functools import cache
2+
from typing import Annotated
3+
4+
from fastapi import Depends
5+
6+
from src.controllers.rocket import RocketController
7+
from src.controllers.motor import MotorController
8+
from src.controllers.environment import EnvironmentController
9+
from src.controllers.flight import FlightController
10+
11+
@cache
12+
def get_rocket_controller() -> RocketController:
13+
"""
14+
Provides a singleton RocketController instance.
15+
16+
The controller is stateless and can be safely reused across requests.
17+
Using functools.cache memoizes this function so a single instance is reused per process; it does not by itself guarantee thread-safe initialization in multi-threaded setups.
18+
19+
Returns:
20+
RocketController: Shared controller instance for rocket operations.
21+
"""
22+
return RocketController()
23+
24+
25+
@cache
26+
def get_motor_controller() -> MotorController:
27+
"""
28+
Provides a singleton MotorController instance.
29+
30+
Returns:
31+
MotorController: Shared controller instance for motor operations.
32+
"""
33+
return MotorController()
34+
35+
36+
@cache
37+
def get_environment_controller() -> EnvironmentController:
38+
"""
39+
Provides a singleton EnvironmentController instance.
40+
41+
Returns:
42+
EnvironmentController: Shared controller instance for environment operations.
43+
"""
44+
return EnvironmentController()
45+
46+
47+
@cache
48+
def get_flight_controller() -> FlightController:
49+
"""
50+
Provides a singleton FlightController instance.
51+
52+
Returns:
53+
FlightController: Shared controller instance for flight operations.
54+
"""
55+
return FlightController()
56+
57+
RocketControllerDep = Annotated[RocketController, Depends(get_rocket_controller)]
58+
MotorControllerDep = Annotated[MotorController, Depends(get_motor_controller)]
59+
EnvironmentControllerDep = Annotated[
60+
EnvironmentController, Depends(get_environment_controller)
61+
]
62+
FlightControllerDep = Annotated[FlightController, Depends(get_flight_controller)]

src/routes/environment.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
EnvironmentRetrieved,
1212
)
1313
from src.models.environment import EnvironmentModel
14-
from src.controllers.environment import EnvironmentController
14+
from src.dependencies import EnvironmentControllerDep
1515

1616
router = APIRouter(
1717
prefix="/environments",
@@ -29,6 +29,7 @@
2929
@router.post("/", status_code=201)
3030
async def create_environment(
3131
environment: EnvironmentModel,
32+
controller: EnvironmentControllerDep,
3233
) -> EnvironmentCreated:
3334
"""
3435
Creates a new environment
@@ -37,26 +38,29 @@ async def create_environment(
3738
``` models.Environment JSON ```
3839
"""
3940
with tracer.start_as_current_span("create_environment"):
40-
controller = EnvironmentController()
4141
return await controller.post_environment(environment)
4242

4343

4444
@router.get("/{environment_id}")
45-
async def read_environment(environment_id: str) -> EnvironmentRetrieved:
45+
async def read_environment(
46+
environment_id: str,
47+
controller: EnvironmentControllerDep,
48+
) -> EnvironmentRetrieved:
4649
"""
4750
Reads an existing environment
4851
4952
## Args
5053
``` environment_id: str ```
5154
"""
5255
with tracer.start_as_current_span("read_environment"):
53-
controller = EnvironmentController()
5456
return await controller.get_environment_by_id(environment_id)
5557

5658

5759
@router.put("/{environment_id}", status_code=204)
5860
async def update_environment(
59-
environment_id: str, environment: EnvironmentModel
61+
environment_id: str,
62+
environment: EnvironmentModel,
63+
controller: EnvironmentControllerDep,
6064
) -> None:
6165
"""
6266
Updates an existing environment
@@ -68,22 +72,23 @@ async def update_environment(
6872
```
6973
"""
7074
with tracer.start_as_current_span("update_environment"):
71-
controller = EnvironmentController()
7275
return await controller.put_environment_by_id(
7376
environment_id, environment
7477
)
7578

7679

7780
@router.delete("/{environment_id}", status_code=204)
78-
async def delete_environment(environment_id: str) -> None:
81+
async def delete_environment(
82+
environment_id: str,
83+
controller: EnvironmentControllerDep,
84+
) -> None:
7985
"""
8086
Deletes an existing environment
8187
8288
## Args
8389
``` environment_id: str ```
8490
"""
8591
with tracer.start_as_current_span("delete_environment"):
86-
controller = EnvironmentController()
8792
return await controller.delete_environment_by_id(environment_id)
8893

8994

@@ -98,7 +103,10 @@ async def delete_environment(environment_id: str) -> None:
98103
status_code=200,
99104
response_class=Response,
100105
)
101-
async def get_rocketpy_environment_binary(environment_id: str):
106+
async def get_rocketpy_environment_binary(
107+
environment_id: str,
108+
controller: EnvironmentControllerDep,
109+
):
102110
"""
103111
Loads rocketpy.environment as a dill binary.
104112
Currently only amd64 architecture is supported.
@@ -110,7 +118,6 @@ async def get_rocketpy_environment_binary(environment_id: str):
110118
headers = {
111119
'Content-Disposition': f'attachment; filename="rocketpy_environment_{environment_id}.dill"'
112120
}
113-
controller = EnvironmentController()
114121
binary = await controller.get_rocketpy_environment_binary(
115122
environment_id
116123
)
@@ -125,6 +132,7 @@ async def get_rocketpy_environment_binary(environment_id: str):
125132
@router.get("/{environment_id}/simulate")
126133
async def get_environment_simulation(
127134
environment_id: str,
135+
controller: EnvironmentControllerDep,
128136
) -> EnvironmentSimulation:
129137
"""
130138
Simulates an environment
@@ -133,5 +141,4 @@ async def get_environment_simulation(
133141
``` environment_id: Environment ID```
134142
"""
135143
with tracer.start_as_current_span("get_environment_simulation"):
136-
controller = EnvironmentController()
137144
return await controller.get_environment_simulation(environment_id)

src/routes/flight.py

Lines changed: 39 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"""
2-
Flight routes
2+
Flight routes with dependency injection for improved performance.
33
"""
44

55
from fastapi import APIRouter, Response
@@ -13,7 +13,7 @@
1313
from src.models.environment import EnvironmentModel
1414
from src.models.flight import FlightModel, FlightWithReferencesRequest
1515
from src.models.rocket import RocketModel
16-
from src.controllers.flight import FlightController
16+
from src.dependencies import FlightControllerDep
1717

1818
router = APIRouter(
1919
prefix="/flights",
@@ -29,21 +29,24 @@
2929

3030

3131
@router.post("/", status_code=201)
32-
async def create_flight(flight: FlightModel) -> FlightCreated:
32+
async def create_flight(
33+
flight: FlightModel,
34+
controller: FlightControllerDep,
35+
) -> FlightCreated:
3336
"""
3437
Creates a new flight
3538
3639
## Args
3740
``` models.Flight JSON ```
3841
"""
3942
with tracer.start_as_current_span("create_flight"):
40-
controller = FlightController()
4143
return await controller.post_flight(flight)
4244

4345

4446
@router.post("/from-references", status_code=201)
4547
async def create_flight_from_references(
4648
payload: FlightWithReferencesRequest,
49+
controller: FlightControllerDep,
4750
) -> FlightCreated:
4851
"""
4952
Creates a flight using existing rocket and environment references.
@@ -56,25 +59,29 @@ async def create_flight_from_references(
5659
```
5760
"""
5861
with tracer.start_as_current_span("create_flight_from_references"):
59-
controller = FlightController()
6062
return await controller.create_flight_from_references(payload)
6163

6264

6365
@router.get("/{flight_id}")
64-
async def read_flight(flight_id: str) -> FlightRetrieved:
66+
async def read_flight(
67+
flight_id: str,
68+
controller: FlightControllerDep,
69+
) -> FlightRetrieved:
6570
"""
6671
Reads an existing flight
6772
6873
## Args
6974
``` flight_id: str ```
7075
"""
7176
with tracer.start_as_current_span("read_flight"):
72-
controller = FlightController()
7377
return await controller.get_flight_by_id(flight_id)
7478

75-
7679
@router.put("/{flight_id}", status_code=204)
77-
async def update_flight(flight_id: str, flight: FlightModel) -> None:
80+
async def update_flight(
81+
flight_id: str,
82+
flight: FlightModel,
83+
controller: FlightControllerDep,
84+
) -> None:
7885
"""
7986
Updates an existing flight
8087
@@ -85,14 +92,14 @@ async def update_flight(flight_id: str, flight: FlightModel) -> None:
8592
```
8693
"""
8794
with tracer.start_as_current_span("update_flight"):
88-
controller = FlightController()
8995
return await controller.put_flight_by_id(flight_id, flight)
9096

9197

9298
@router.put("/{flight_id}/from-references", status_code=204)
9399
async def update_flight_from_references(
94100
flight_id: str,
95101
payload: FlightWithReferencesRequest,
102+
controller: FlightControllerDep,
96103
) -> None:
97104
"""
98105
Updates a flight using existing rocket and environment references.
@@ -106,22 +113,22 @@ async def update_flight_from_references(
106113
```
107114
"""
108115
with tracer.start_as_current_span("update_flight_from_references"):
109-
controller = FlightController()
110116
return await controller.update_flight_from_references(
111117
flight_id, payload
112118
)
113119

114-
115120
@router.delete("/{flight_id}", status_code=204)
116-
async def delete_flight(flight_id: str) -> None:
121+
async def delete_flight(
122+
flight_id: str,
123+
controller: FlightControllerDep,
124+
) -> None:
117125
"""
118126
Deletes an existing flight
119127
120128
## Args
121129
``` flight_id: str ```
122130
"""
123131
with tracer.start_as_current_span("delete_flight"):
124-
controller = FlightController()
125132
return await controller.delete_flight_by_id(flight_id)
126133

127134

@@ -136,7 +143,11 @@ async def delete_flight(flight_id: str) -> None:
136143
status_code=200,
137144
response_class=Response,
138145
)
139-
async def get_rocketpy_flight_binary(flight_id: str):
146+
147+
async def get_rocketpy_flight_binary(
148+
flight_id: str,
149+
controller: FlightControllerDep,
150+
):
140151
"""
141152
Loads rocketpy.flight as a dill binary.
142153
Currently only amd64 architecture is supported.
@@ -145,7 +156,6 @@ async def get_rocketpy_flight_binary(flight_id: str):
145156
``` flight_id: str ```
146157
"""
147158
with tracer.start_as_current_span("get_rocketpy_flight_binary"):
148-
controller = FlightController()
149159
headers = {
150160
'Content-Disposition': f'attachment; filename="rocketpy_flight_{flight_id}.dill"'
151161
}
@@ -160,7 +170,9 @@ async def get_rocketpy_flight_binary(flight_id: str):
160170

161171
@router.put("/{flight_id}/environment", status_code=204)
162172
async def update_flight_environment(
163-
flight_id: str, environment: EnvironmentModel
173+
flight_id: str,
174+
environment: EnvironmentModel,
175+
controller: FlightControllerDep,
164176
) -> None:
165177
"""
166178
Updates flight environment
@@ -172,14 +184,17 @@ async def update_flight_environment(
172184
```
173185
"""
174186
with tracer.start_as_current_span("update_flight_environment"):
175-
controller = FlightController()
176187
return await controller.update_environment_by_flight_id(
177188
flight_id, environment=environment
178189
)
179190

180191

181192
@router.put("/{flight_id}/rocket", status_code=204)
182-
async def update_flight_rocket(flight_id: str, rocket: RocketModel) -> None:
193+
async def update_flight_rocket(
194+
flight_id: str,
195+
rocket: RocketModel,
196+
controller: FlightControllerDep,
197+
) -> None:
183198
"""
184199
Updates flight rocket.
185200
@@ -190,21 +205,21 @@ async def update_flight_rocket(flight_id: str, rocket: RocketModel) -> None:
190205
```
191206
"""
192207
with tracer.start_as_current_span("update_flight_rocket"):
193-
controller = FlightController()
194208
return await controller.update_rocket_by_flight_id(
195209
flight_id,
196210
rocket=rocket,
197211
)
198212

199-
200213
@router.get("/{flight_id}/simulate")
201-
async def get_flight_simulation(flight_id: str) -> FlightSimulation:
214+
async def get_flight_simulation(
215+
flight_id: str,
216+
controller: FlightControllerDep,
217+
) -> FlightSimulation:
202218
"""
203219
Simulates a flight
204220
205221
## Args
206222
``` flight_id: Flight ID ```
207223
"""
208224
with tracer.start_as_current_span("get_flight_simulation"):
209-
controller = FlightController()
210-
return await controller.get_flight_simulation(flight_id)
225+
return await controller.get_flight_simulation(flight_id)

0 commit comments

Comments
 (0)