Skip to content

Commit ee13c2f

Browse files
MNT: address PR review comments from CodeRabbit and Copilot
- Fix implicit string concatenation in notebook source (Pylint W1404) - Update FlightImported.message to say ".rpy file" instead of "binary" - Add 10 MB upload size guard with HTTP 413 on POST /flights/upload - Extract tanks for LIQUID/HYBRID motors in _extract_motor to satisfy MotorModel validation (new _extract_tanks and _to_float helpers) - Add comment explaining default_fins schema fallback - Tighten test assertion on import success message - Add pre-yield cache_clear() in environments test fixture Made-with: Cursor
1 parent 50be27f commit ee13c2f

File tree

5 files changed

+130
-23
lines changed

5 files changed

+130
-23
lines changed

src/routes/flight.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,14 @@
44

55
import json
66

7-
from fastapi import APIRouter, Response, UploadFile, File
7+
from fastapi import (
8+
APIRouter,
9+
File,
10+
HTTPException,
11+
Response,
12+
UploadFile,
13+
status,
14+
)
815
from opentelemetry import trace
916

1017
from src.views.flight import (
@@ -30,6 +37,8 @@
3037

3138
tracer = trace.get_tracer(__name__)
3239

40+
MAX_RPY_UPLOAD_BYTES = 10 * 1024 * 1024 # 10 MB
41+
3342

3443
@router.post("/", status_code=201)
3544
async def create_flight(
@@ -186,21 +195,26 @@ async def get_rocketpy_flight_rpy(
186195
)
187196
async def import_flight_from_rpy(
188197
file: UploadFile = File(...),
189-
controller: FlightControllerDep = None,
198+
controller: FlightControllerDep = None, # noqa: B008
190199
) -> FlightImported:
191200
"""
192201
Upload a ``.rpy`` JSON file containing a RocketPy Flight.
193202
194203
The file is deserialized and decomposed into its
195204
constituent objects (Environment, Motor, Rocket, Flight).
196205
Each object is persisted as a normal JSON model and the
197-
corresponding IDs are returned.
206+
corresponding IDs are returned. Maximum upload size is 10 MB.
198207
199208
## Args
200209
``` file: .rpy JSON upload ```
201210
"""
202211
with tracer.start_as_current_span("import_flight_from_rpy"):
203-
content = await file.read()
212+
content = await file.read(MAX_RPY_UPLOAD_BYTES + 1)
213+
if len(content) > MAX_RPY_UPLOAD_BYTES:
214+
raise HTTPException(
215+
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
216+
detail="Uploaded .rpy file exceeds 10 MB limit.",
217+
)
204218
return await controller.import_flight_from_rpy(content)
205219

206220

src/services/flight.py

Lines changed: 108 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,15 @@
55

66
from rocketpy.simulation.flight import Flight as RocketPyFlight
77
from rocketpy._encoders import RocketPyEncoder, RocketPyDecoder
8+
from rocketpy.mathutils.function import Function
89
from rocketpy.motors.solid_motor import SolidMotor
910
from rocketpy.motors.liquid_motor import LiquidMotor
1011
from rocketpy.motors.hybrid_motor import HybridMotor
12+
from rocketpy import (
13+
LevelBasedTank,
14+
MassBasedTank,
15+
UllageBasedTank,
16+
)
1117
from rocketpy.rocket.aero_surface import (
1218
NoseCone as RocketPyNoseCone,
1319
TrapezoidalFins as RocketPyTrapezoidalFins,
@@ -27,6 +33,7 @@
2733
Tail,
2834
Parachute,
2935
)
36+
from src.models.sub.tanks import MotorTank, TankFluids, TankKinds
3037
from src.views.flight import FlightSimulation
3138
from src.views.rocket import RocketSimulation
3239
from src.views.motor import MotorSimulation
@@ -169,22 +176,27 @@ def _extract_motor(motor) -> MotorModel:
169176
),
170177
}
171178

179+
grain_fields = {
180+
"grain_number": motor.grain_number,
181+
"grain_density": motor.grain_density,
182+
"grain_outer_radius": motor.grain_outer_radius,
183+
"grain_initial_inner_radius": (motor.grain_initial_inner_radius),
184+
"grain_initial_height": motor.grain_initial_height,
185+
"grain_separation": motor.grain_separation,
186+
"grains_center_of_mass_position": (
187+
motor.grains_center_of_mass_position
188+
),
189+
"throat_radius": motor.throat_radius,
190+
}
191+
172192
match kind:
173-
case MotorKinds.SOLID | MotorKinds.HYBRID:
174-
data |= {
175-
"grain_number": motor.grain_number,
176-
"grain_density": motor.grain_density,
177-
"grain_outer_radius": (motor.grain_outer_radius),
178-
"grain_initial_inner_radius": (
179-
motor.grain_initial_inner_radius
180-
),
181-
"grain_initial_height": (motor.grain_initial_height),
182-
"grain_separation": motor.grain_separation,
183-
"grains_center_of_mass_position": (
184-
motor.grains_center_of_mass_position
185-
),
186-
"throat_radius": motor.throat_radius,
187-
}
193+
case MotorKinds.SOLID:
194+
data |= grain_fields
195+
case MotorKinds.HYBRID:
196+
data |= grain_fields
197+
data["tanks"] = FlightService._extract_tanks(motor)
198+
case MotorKinds.LIQUID:
199+
data["tanks"] = FlightService._extract_tanks(motor)
188200
case MotorKinds.GENERIC:
189201
data |= {
190202
"chamber_radius": getattr(motor, "chamber_radius", None),
@@ -202,6 +214,83 @@ def _extract_motor(motor) -> MotorModel:
202214

203215
return MotorModel(**data)
204216

217+
@staticmethod
218+
def _to_float(value) -> float:
219+
"""Extract a plain float from a RocketPy Function or scalar."""
220+
match value:
221+
case Function():
222+
return float(value(0))
223+
case _:
224+
return float(value)
225+
226+
@staticmethod
227+
def _extract_tanks(motor) -> list[MotorTank]:
228+
tanks: list[MotorTank] = []
229+
for entry in motor.positioned_tanks:
230+
tank, position = entry["tank"], entry["position"]
231+
232+
match tank:
233+
case LevelBasedTank():
234+
tank_kind = TankKinds.LEVEL
235+
case MassBasedTank():
236+
tank_kind = TankKinds.MASS
237+
case UllageBasedTank():
238+
tank_kind = TankKinds.ULLAGE
239+
case _:
240+
tank_kind = TankKinds.MASS_FLOW
241+
242+
geometry = [
243+
(bounds, float(func(0)))
244+
for bounds, func in tank.geometry.geometry.items()
245+
]
246+
247+
data: dict = {
248+
"geometry": geometry,
249+
"gas": TankFluids(
250+
name=tank.gas.name,
251+
density=tank.gas.density,
252+
),
253+
"liquid": TankFluids(
254+
name=tank.liquid.name,
255+
density=tank.liquid.density,
256+
),
257+
"flux_time": tank.flux_time,
258+
"position": position,
259+
"discretize": tank.discretize,
260+
"tank_kind": tank_kind,
261+
"name": tank.name,
262+
}
263+
264+
_f = FlightService._to_float
265+
match tank_kind:
266+
case TankKinds.LEVEL:
267+
data["liquid_height"] = _f(tank.liquid_height)
268+
case TankKinds.MASS:
269+
data["liquid_mass"] = _f(tank.liquid_mass)
270+
data["gas_mass"] = _f(tank.gas_mass)
271+
case TankKinds.MASS_FLOW:
272+
data |= {
273+
"gas_mass_flow_rate_in": _f(
274+
tank.gas_mass_flow_rate_in
275+
),
276+
"gas_mass_flow_rate_out": _f(
277+
tank.gas_mass_flow_rate_out
278+
),
279+
"liquid_mass_flow_rate_in": _f(
280+
tank.liquid_mass_flow_rate_in
281+
),
282+
"liquid_mass_flow_rate_out": _f(
283+
tank.liquid_mass_flow_rate_out
284+
),
285+
"initial_liquid_mass": (tank.initial_liquid_mass),
286+
"initial_gas_mass": (tank.initial_gas_mass),
287+
}
288+
case TankKinds.ULLAGE:
289+
data["ullage"] = _f(tank.ullage)
290+
291+
tanks.append(MotorTank(**data))
292+
return tanks
293+
205294
@staticmethod
206295
def _drag_to_list(fn) -> list:
207296
match getattr(fn, "source", None):
@@ -293,6 +382,8 @@ def _extract_rocket(rocket, motor_model: MotorModel) -> RocketModel:
293382
rocket.I_33_without_motor,
294383
)
295384

385+
# Schema requires at least one Fins entry; n=0 means
386+
# no physical fins (safe for downstream aero calculations).
296387
default_fins = [
297388
Fins(
298389
fins_kind="trapezoidal",
@@ -359,7 +450,7 @@ def _extract_flight(
359450
)
360451

361452
# ------------------------------------------------------------------
362-
# Simulation & binary
453+
# Simulation & export
363454
# ------------------------------------------------------------------
364455

365456
def get_flight_simulation(self) -> FlightSimulation:
@@ -432,7 +523,7 @@ def generate_notebook(flight_id: str) -> dict:
432523
"metadata": {},
433524
"outputs": [],
434525
"source": [
435-
"from rocketpy.utilities import " "load_from_rpy\n",
526+
"from rocketpy.utilities import load_from_rpy\n",
436527
"import matplotlib\n",
437528
],
438529
},

src/views/flight.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ class FlightCreated(ApiBaseView):
151151

152152

153153
class FlightImported(ApiBaseView):
154-
message: str = "Flight successfully imported from binary"
154+
message: str = "Flight successfully imported from .rpy file"
155155
flight_id: str
156156
rocket_id: str
157157
motor_id: str

tests/unit/test_routes/test_environments_route.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ def mock_controller_instance():
3838

3939
mock_class.return_value = mock_controller
4040

41+
get_environment_controller.cache_clear()
42+
4143
yield mock_controller
4244

4345
get_environment_controller.cache_clear()

tests/unit/test_routes/test_flights_route.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -568,7 +568,7 @@ def test_import_flight_from_rpy(mock_controller_instance):
568568
assert body['rocket_id'] == 'r1'
569569
assert body['motor_id'] == 'm1'
570570
assert body['environment_id'] == 'e1'
571-
assert 'imported' in body['message'].lower()
571+
assert body['message'] == "Flight successfully imported from .rpy file"
572572
mock_controller_instance.import_flight_from_rpy.assert_called_once_with(
573573
rpy_content
574574
)

0 commit comments

Comments
 (0)