ENH: portable .rpy flight import/export and notebook generation#70
ENH: portable .rpy flight import/export and notebook generation#70GabrielBarberini wants to merge 6 commits intomasterfrom
Conversation
…57) Replace dill-based binary serialization with RocketPy's native JSON-based .rpy format (RocketPyEncoder/RocketPyDecoder), making flight export and import architecture-, OS-, and Python-version- agnostic. Add POST /flights/upload to import .rpy files by decomposing them into Environment, Motor, Rocket and Flight models persisted through the standard CRUD pipeline. Add GET /flights/{id}/notebook to export a flight as a Jupyter notebook that loads the .rpy file via load_from_rpy. Closes #56, closes #57. Made-with: Cursor
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds .rpy JSON import/export and Jupyter notebook export for RocketPy flights: new serialization/deserialization and model extraction in FlightService, controller and route handlers for upload/download and persistence, a new FlightImported view, tests updates, and Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor Client
participant Route as "Route (import_flight_from_rpy)"
participant Controller as "FlightController"
participant Service as "FlightService"
participant Repos as "Repositories"
Client->>Route: POST .rpy file (multipart)
Route->>Controller: import_flight_from_rpy(content)
Controller->>Service: FlightService.from_rpy(content)
Service->>Service: deserialize via RocketPyDecoder
Service->>Service: extract_models() -> (Env, Motor, Rocket, Flight)
Controller->>Repos: persist EnvironmentModel
Repos-->>Controller: environment_id
Controller->>Repos: persist MotorModel
Repos-->>Controller: motor_id
Controller->>Repos: persist RocketModel
Repos-->>Controller: rocket_id
Controller->>Repos: persist FlightModel
Repos-->>Controller: flight_id
Controller-->>Route: return FlightImported{flight_id, rocket_id, motor_id, environment_id}
Route-->>Client: 200 JSON response
sequenceDiagram
autonumber
actor Client
participant Route as "Route (export endpoints)"
participant Controller as "FlightController"
participant Service as "FlightService"
Client->>Route: GET /flights/<id>/rpy or GET /flights/<id>/notebook
Route->>Controller: get_rocketpy_flight_rpy() / get_flight_notebook()
Controller->>Service: get_flight_rpy() / generate_notebook()
alt .rpy export
Service->>Service: encode flight via RocketPyEncoder -> bytes (JSON)
Service-->>Controller: bytes
Controller-->>Route: set application/json, filename .rpy
else notebook export
Service->>Service: build notebook dict
Service-->>Controller: dict
Controller-->>Route: set application/x-ipynb+json, filename .ipynb
end
Route-->>Client: 200 with serialized payload and headers
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
tests/unit/test_routes/test_environments_route.py (1)
28-43:⚠️ Potential issue | 🟡 MinorMissing
cache_clear()call beforeyieldin fixture.The
mock_controller_instancefixture only callsget_environment_controller.cache_clear()after yielding, but not before. Other test files in this PR (e.g.,test_motors_route.pyandtest_rockets_route.py) callcache_clear()both before and after yield to ensure a clean state. This inconsistency could lead to stale cached controller instances when tests run in sequence.Proposed fix
`@pytest.fixture`(autouse=True) def 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()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/test_routes/test_environments_route.py` around lines 28 - 43, The fixture mock_controller_instance should clear the cached controller both before and after yielding to avoid stale instances: call get_environment_controller.cache_clear() once immediately before creating/patching EnvironmentController (or right after patching but before yield) in the mock_controller_instance fixture, and keep the existing cache_clear() after yield to restore clean state; update the mock_controller_instance fixture to perform this pre-yield cache_clear() so it matches the behavior in test_motors_route.py and test_rockets_route.py.
🧹 Nitpick comments (4)
src/services/flight.py (2)
93-95: PreferTypeErrorfor type validation failure.The static analysis correctly identifies that
TypeErroris more appropriate thanValueErrorwhen validating that the deserialized object is of the expected type.Proposed fix
if not isinstance(flight, RocketPyFlight): - raise ValueError("File does not contain a RocketPy Flight object") + raise TypeError( + "Deserialized object is not a RocketPy Flight" + )🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/flight.py` around lines 93 - 95, Replace the inappropriate ValueError raised when the deserialized object is not the expected type: in the method that checks "if not isinstance(flight, RocketPyFlight)" (the code that currently raises ValueError("File does not contain a RocketPy Flight object") and returns cls(flight=flight)), change the exception to TypeError and update the message if desired (e.g., indicate the object is not a RocketPyFlight) so type validation failures raise TypeError instead of ValueError.
296-320: Consider documenting the rationale for default fins fallback.The
default_finswithn=0(zero fins) serves as a schema-satisfying placeholder when no fins are extracted. While this works, consider adding a brief comment explaining this is a fallback for flights where aerodynamic surfaces weren't fully captured, to aid future maintainers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/flight.py` around lines 296 - 320, The code creates a placeholder default_fins = [Fins(..., n=0, ...)] used when fins_list is empty but has no explanatory note; add a concise inline comment (or a brief docstring above the block) next to default_fins explaining that this is an intentional schema-satisfying fallback representing "no fins" (n=0) so RocketModel.fins always has a list when fins extraction fails, and mention any implications for downstream aerodynamic calculations that should treat n=0 as "no aerodynamic surfaces." Reference the default_fins variable, the Fins constructor usage, and the RocketModel(..., fins=...) assignment when placing the comment.src/controllers/flight.py (1)
203-215: Consider partial failure handling for multi-model persistence.The import persists Environment, Motor, Rocket, and Flight sequentially without a transaction. If persistence fails mid-way (e.g., Rocket creation fails), earlier models (Environment, Motor) become orphaned in the database with no referencing Flight.
For an MVP this is acceptable, but consider documenting this limitation or adding cleanup logic for robustness.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/controllers/flight.py` around lines 203 - 215, The import currently calls flight_service.extract_models() and then sequentially persists EnvironmentModel, MotorModel, RocketModel, and FlightModel via self._persist_model(), which can leave orphaned records on mid-way failure; either wrap the multi-model persistence in a DB transaction so all four inserts commit or roll back atomically, or add compensating cleanup logic that deletes any successfully created models if a later persist raises an exception (ensure cleanup references the created ids and handles partial failures), and update FlightImported construction to only occur after successful commit; relevant symbols: extract_models, _persist_model, EnvironmentModel, MotorModel, RocketModel, FlightModel, FlightImported.tests/unit/test_routes/test_flights_route.py (1)
567-571: Tighten the success-message assertion to protect the API contract.The current check (
"imported" in message) is permissive and won’t catch stale wording like “from binary”.✅ Suggested test refinement
assert body['flight_id'] == 'f1' assert body['rocket_id'] == 'r1' assert body['motor_id'] == 'm1' assert body['environment_id'] == 'e1' - assert 'imported' in body['message'].lower() + assert body['message'] == "Flight successfully imported from .rpy file"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/test_routes/test_flights_route.py` around lines 567 - 571, The success-message assertion is too permissive; replace the loose substring check on body['message'] with a precise contract assertion to prevent regressions (e.g., instead of assert 'imported' in body['message'].lower(), assert the exact expected string using the returned id like assert body['message'] == f"Flight {body['flight_id']} imported." or match whatever canonical message the API returns). Update the assertion in tests/unit/test_routes/test_flights_route.py to compare the full message string using body['flight_id'] (and other IDs if part of the canonical message) so the test fails on any wording change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/routes/flight.py`:
- Around line 187-204: The handler import_flight_from_rpy currently calls await
file.read() without bounds which can OOM; modify import_flight_from_rpy to
enforce a hard upload size cap (e.g. MAX_RPY_SIZE bytes) by reading the
UploadFile in controlled chunks from file.file (or using a streamed read) and
summing bytes, returning an HTTP 413 (Payload Too Large) immediately if the cap
is exceeded; keep the tracer.start_as_current_span usage and only call
controller.import_flight_from_rpy(content) after the size-checked content has
been fully accumulated (or reject early).
In `@src/services/flight.py`:
- Around line 434-437: The "source" list in src/services/flight.py contains an
implicit string concatenation ("from rocketpy.utilities import "
"load_from_rpy\n") which Pylint flags; locate the "source" list entry and
replace the two adjacent string literals with a single explicit string (e.g.
"from rocketpy.utilities import load_from_rpy\n") or explicitly concatenate them
with + so the list contains only proper string elements.
In `@src/views/flight.py`:
- Around line 153-154: Update the FlightImported.message string to reflect that
the flight is imported from an .rpy JSON rather than "binary": locate the
FlightImported class and change the message attribute (message: str = "Flight
successfully imported from binary") to a wording like "Flight successfully
imported from .rpy JSON" or similar to match the endpoint behavior.
---
Outside diff comments:
In `@tests/unit/test_routes/test_environments_route.py`:
- Around line 28-43: The fixture mock_controller_instance should clear the
cached controller both before and after yielding to avoid stale instances: call
get_environment_controller.cache_clear() once immediately before
creating/patching EnvironmentController (or right after patching but before
yield) in the mock_controller_instance fixture, and keep the existing
cache_clear() after yield to restore clean state; update the
mock_controller_instance fixture to perform this pre-yield cache_clear() so it
matches the behavior in test_motors_route.py and test_rockets_route.py.
---
Nitpick comments:
In `@src/controllers/flight.py`:
- Around line 203-215: The import currently calls
flight_service.extract_models() and then sequentially persists EnvironmentModel,
MotorModel, RocketModel, and FlightModel via self._persist_model(), which can
leave orphaned records on mid-way failure; either wrap the multi-model
persistence in a DB transaction so all four inserts commit or roll back
atomically, or add compensating cleanup logic that deletes any successfully
created models if a later persist raises an exception (ensure cleanup references
the created ids and handles partial failures), and update FlightImported
construction to only occur after successful commit; relevant symbols:
extract_models, _persist_model, EnvironmentModel, MotorModel, RocketModel,
FlightModel, FlightImported.
In `@src/services/flight.py`:
- Around line 93-95: Replace the inappropriate ValueError raised when the
deserialized object is not the expected type: in the method that checks "if not
isinstance(flight, RocketPyFlight)" (the code that currently raises
ValueError("File does not contain a RocketPy Flight object") and returns
cls(flight=flight)), change the exception to TypeError and update the message if
desired (e.g., indicate the object is not a RocketPyFlight) so type validation
failures raise TypeError instead of ValueError.
- Around line 296-320: The code creates a placeholder default_fins = [Fins(...,
n=0, ...)] used when fins_list is empty but has no explanatory note; add a
concise inline comment (or a brief docstring above the block) next to
default_fins explaining that this is an intentional schema-satisfying fallback
representing "no fins" (n=0) so RocketModel.fins always has a list when fins
extraction fails, and mention any implications for downstream aerodynamic
calculations that should treat n=0 as "no aerodynamic surfaces." Reference the
default_fins variable, the Fins constructor usage, and the RocketModel(...,
fins=...) assignment when placing the comment.
In `@tests/unit/test_routes/test_flights_route.py`:
- Around line 567-571: The success-message assertion is too permissive; replace
the loose substring check on body['message'] with a precise contract assertion
to prevent regressions (e.g., instead of assert 'imported' in
body['message'].lower(), assert the exact expected string using the returned id
like assert body['message'] == f"Flight {body['flight_id']} imported." or match
whatever canonical message the API returns). Update the assertion in
tests/unit/test_routes/test_flights_route.py to compare the full message string
using body['flight_id'] (and other IDs if part of the canonical message) so the
test fails on any wording change.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (12)
requirements.txtsrc/controllers/flight.pysrc/dependencies.pysrc/routes/flight.pysrc/routes/rocket.pysrc/services/flight.pysrc/services/rocket.pysrc/views/flight.pytests/unit/test_routes/test_environments_route.pytests/unit/test_routes/test_flights_route.pytests/unit/test_routes/test_motors_route.pytests/unit/test_routes/test_rockets_route.py
There was a problem hiding this comment.
Pull request overview
This PR enhances the Flight API by replacing architecture-pinned dill flight export/import with RocketPy’s portable JSON-based .rpy format and adding a new endpoint to generate a downloadable Jupyter notebook for interactive flight analysis.
Changes:
- Replace RocketPy flight export from dill binary to
.rpyJSON (RocketPyEncoder/RocketPyDecoder) and add.rpyupload/import flow. - Add
GET /flights/{id}/notebookto return a generated.ipynbthat loads the exported.rpyand callsflight.all_info(). - Update routes/views/tests and add
python-multipartto support file uploads.
Reviewed changes
Copilot reviewed 10 out of 12 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
src/services/flight.py |
Adds .rpy (JSON) serialization/deserialization, model extraction helpers, and notebook generation. |
src/controllers/flight.py |
Adds import/persist pipeline for .rpy uploads and notebook generation handler. |
src/routes/flight.py |
Switches /rocketpy export to JSON .rpy, adds /upload and /notebook endpoints. |
src/views/flight.py |
Introduces FlightImported response model for .rpy import. |
src/dependencies.py |
Formatting/refactor of dependency type aliases. |
requirements.txt |
Adds python-multipart for file upload support. |
tests/unit/test_routes/test_flights_route.py |
Updates export tests to .rpy/JSON, adds tests for upload import and notebook export. |
tests/unit/test_routes/test_rockets_route.py |
Cleans up unused mock imports/whitespace. |
tests/unit/test_routes/test_motors_route.py |
Cleans up unused mock imports/whitespace. |
tests/unit/test_routes/test_environments_route.py |
Cleans up unused mock imports/whitespace. |
src/routes/rocket.py / src/services/rocket.py |
Minor whitespace/formatting adjustments. |
Comments suppressed due to low confidence (4)
src/services/flight.py:465
- The generated notebook hard-codes
language_info.versionto3.12.0. If the API runtime uses a different Python version, this metadata becomes inaccurate and can confuse users/debugging. Consider omitting the version field or deriving it fromsys.version_infoat generation time.
"language_info": {
"name": "python",
"version": "3.12.0",
},
src/views/flight.py:156
FlightImported.messagesays the flight was imported "from binary", but this PR switches imports to JSON-based.rpyfiles. This user-facing message should be updated to refer to.rpy/JSON (or just "file") to avoid misleading API consumers.
class FlightImported(ApiBaseView):
message: str = "Flight successfully imported from binary"
flight_id: str
rocket_id: str
src/routes/flight.py:190
import_flight_from_rpydeclarescontroller: FlightControllerDep = None, unlike the other routes in this module which rely on dependency injection without a default. Dropping the= Nonekeeps the signature consistent and avoids implying the dependency is optional.
async def import_flight_from_rpy(
file: UploadFile = File(...),
controller: FlightControllerDep = None,
) -> FlightImported:
src/routes/flight.py:204
- This endpoint reads the entire uploaded file into memory with
await file.read(). For potentially large.rpyuploads this can cause high memory usage or worker OOM. Consider enforcing an upload size limit (e.g., via server/middleware) and/or streaming/reading with a capped buffer before passing to the decoder.
with tracer.start_as_current_span("import_flight_from_rpy"):
content = await file.read()
return await controller.import_flight_from_rpy(content)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- 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
|
All actionable review comments from CodeRabbit and Copilot have been addressed in ee13c2f:
Intentionally not addressed:
All 155 tests pass, Pylint 10/10, Flake8 clean. |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (1)
tests/unit/test_routes/test_flights_route.py (1)
545-602: Add a test for oversized.rpyuploads (HTTP 413).The route now enforces a 10 MB cap, but this branch isn’t covered yet.
💡 Proposed test
+def test_import_flight_from_rpy_payload_too_large(mock_controller_instance): + oversized = b"a" * (10 * 1024 * 1024 + 1) + response = client.post( + '/flights/upload', + files={'file': ('large.rpy', oversized, 'application/json')}, + ) + assert response.status_code == 413 + assert response.json() == { + 'detail': 'Uploaded .rpy file exceeds 10 MB limit.' + } + mock_controller_instance.import_flight_from_rpy.assert_not_called()🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/test_routes/test_flights_route.py` around lines 545 - 602, Add a test to cover the 10 MB upload cap by creating a new function (e.g., test_import_flight_from_rpy_oversized) that posts a file larger than 10 * 1024 * 1024 bytes to '/flights/upload' (use a bytes object like b'a' * (10 * 1024 * 1024 + 1)), asserts response.status_code == 413, and verifies mock_controller_instance.import_flight_from_rpy was not called; place it alongside the existing tests (test_import_flight_from_rpy, test_import_flight_from_rpy_invalid, test_import_flight_from_rpy_server_error) so the oversized branch is covered.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/routes/flight.py`:
- Around line 191-194: The route decorator's responses map currently lists 201
and 422 but omits HTTP 413 which the handler returns for oversized uploads;
update the decorator's responses dict (the same one shown with keys 201 and 422)
to include a 413 entry (e.g. 413: {"description": "Uploaded file too large" or
"Payload too large"}) so the OpenAPI/schema matches the handler behavior. Ensure
the new 413 entry is added alongside the existing 201 and 422 keys in the
responses mapping in src/routes/flight.py.
In `@src/services/flight.py`:
- Around line 428-438: The _extract_flight function builds the optional dict via
a comprehension that omits the verbose attribute, causing verbose to be lost;
update the attrs tuple in the optional comprehension (the one listing
"max_time", "max_time_step", "min_time_step", "rtol", "atol") to include
"verbose" so that getattr(flight, "verbose", None) is considered and, if not
None, gets preserved in the returned optional dict used for .rpy import/export.
- Around line 179-200: The code unconditionally constructs grain_fields which
accesses motor.grain_* attributes and can raise AttributeError for non-grain
motors; change FlightService's extraction logic so grain_fields is only built
when kind requires it (i.e., inside the MotorKinds.SOLID and MotorKinds.HYBRID
branches) or guard access with hasattr checks or a try/except around the
attribute reads; update the match block to either (a) compute grain_fields
within the SOLID and HYBRID cases and then merge into data, or (b) wrap the
original grain_fields creation in a safe accessor that returns None/omits keys
if attributes are missing, while keeping FlightService._extract_tanks usage
unchanged for LIQUID/GENERIC.
---
Nitpick comments:
In `@tests/unit/test_routes/test_flights_route.py`:
- Around line 545-602: Add a test to cover the 10 MB upload cap by creating a
new function (e.g., test_import_flight_from_rpy_oversized) that posts a file
larger than 10 * 1024 * 1024 bytes to '/flights/upload' (use a bytes object like
b'a' * (10 * 1024 * 1024 + 1)), asserts response.status_code == 413, and
verifies mock_controller_instance.import_flight_from_rpy was not called; place
it alongside the existing tests (test_import_flight_from_rpy,
test_import_flight_from_rpy_invalid, test_import_flight_from_rpy_server_error)
so the oversized branch is covered.
ℹ️ Review info
Configuration used: defaults
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
src/routes/flight.pysrc/services/flight.pysrc/views/flight.pytests/unit/test_routes/test_environments_route.pytests/unit/test_routes/test_flights_route.py
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
- Add HTTP 413 to /upload route OpenAPI responses schema - Move grain_fields construction inside SOLID|HYBRID match branch to avoid AttributeError on LIQUID/GENERIC motors - Add test for oversized .rpy upload returning HTTP 413 Made-with: Cursor
|
All remaining review comments addressed in 5eb763f:
All 156 tests pass, Pylint 10/10, Flake8 clean. |
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/services/flight.py (1)
179-197:⚠️ Potential issue | 🔴 CriticalUnfixed critical bug:
grain_fieldsbuilt unconditionally will crash on LIQUID/GENERIC motors.This issue was flagged in a previous review and acknowledged, but the fix was not applied. Accessing
motor.grain_number,motor.grain_density, etc. at lines 179-189 will raiseAttributeErrorforLiquidMotorandGenericMotorinstances that lack these attributes.Move
grain_fieldsconstruction inside theSOLID | HYBRIDbranch:Proposed fix
- grain_fields = { - "grain_number": motor.grain_number, - "grain_density": motor.grain_density, - "grain_outer_radius": motor.grain_outer_radius, - "grain_initial_inner_radius": (motor.grain_initial_inner_radius), - "grain_initial_height": motor.grain_initial_height, - "grain_separation": motor.grain_separation, - "grains_center_of_mass_position": ( - motor.grains_center_of_mass_position - ), - "throat_radius": motor.throat_radius, - } - match kind: - case MotorKinds.SOLID: - data |= grain_fields - case MotorKinds.HYBRID: - data |= grain_fields + case MotorKinds.SOLID | MotorKinds.HYBRID: + data |= { + "grain_number": motor.grain_number, + "grain_density": motor.grain_density, + "grain_outer_radius": motor.grain_outer_radius, + "grain_initial_inner_radius": ( + motor.grain_initial_inner_radius + ), + "grain_initial_height": motor.grain_initial_height, + "grain_separation": motor.grain_separation, + "grains_center_of_mass_position": ( + motor.grains_center_of_mass_position + ), + "throat_radius": motor.throat_radius, + } + if kind is MotorKinds.HYBRID: + data["tanks"] = FlightService._extract_tanks(motor) - data["tanks"] = FlightService._extract_tanks(motor) case MotorKinds.LIQUID:🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/flight.py` around lines 179 - 197, The code constructs grain_fields unconditionally using attributes like motor.grain_number which will raise AttributeError for LiquidMotor/GenericMotor; move the grain_fields construction inside the branch that handles solids/hybrids so those attributes are only accessed when kind is MotorKinds.SOLID or MotorKinds.HYBRID, then merge into data (and add tanks via FlightService._extract_tanks only for HYBRID); update the match block to build grain_fields locally within the SOLID and HYBRID cases rather than before the match.
🧹 Nitpick comments (1)
src/services/flight.py (1)
93-101: Consider catchingJSONDecodeErrorfor consistent error reporting.If the input bytes are not valid JSON,
json.loads(content)at line 93 will raiseJSONDecodeError. Consider wrapping in a try/except to provide a consistentValueErrorwith a user-friendly message, matching the pattern at line 101.+ try: + data = json.loads(content) + except json.JSONDecodeError as exc: + raise ValueError("Invalid .rpy file: malformed JSON") from exc - data = json.loads(content)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/services/flight.py` around lines 93 - 101, The initial json.loads(content) can raise json.JSONDecodeError and should be caught so the function raises a consistent ValueError; wrap the first json.loads(content) (and optionally the subsequent json.loads(..., cls=RocketPyDecoder)) in a try/except that catches json.JSONDecodeError and re-raises a ValueError with a clear user-friendly message (e.g., "Input is not valid JSON" or similar) before proceeding to validate the resulting object as a RocketPyFlight; update handling around the RocketPyDecoder / RocketPyFlight check to preserve the existing ValueError pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@src/services/flight.py`:
- Around line 179-197: The code constructs grain_fields unconditionally using
attributes like motor.grain_number which will raise AttributeError for
LiquidMotor/GenericMotor; move the grain_fields construction inside the branch
that handles solids/hybrids so those attributes are only accessed when kind is
MotorKinds.SOLID or MotorKinds.HYBRID, then merge into data (and add tanks via
FlightService._extract_tanks only for HYBRID); update the match block to build
grain_fields locally within the SOLID and HYBRID cases rather than before the
match.
---
Nitpick comments:
In `@src/services/flight.py`:
- Around line 93-101: The initial json.loads(content) can raise
json.JSONDecodeError and should be caught so the function raises a consistent
ValueError; wrap the first json.loads(content) (and optionally the subsequent
json.loads(..., cls=RocketPyDecoder)) in a try/except that catches
json.JSONDecodeError and re-raises a ValueError with a clear user-friendly
message (e.g., "Input is not valid JSON" or similar) before proceeding to
validate the resulting object as a RocketPyFlight; update handling around the
RocketPyDecoder / RocketPyFlight check to preserve the existing ValueError
pattern.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
tests/unit/test_routes/test_flights_route.py (1)
545-656: Add one real integration test for these new endpoints.These cases only verify route wiring against mocked controllers, so they will not catch regressions in multipart parsing, upload-limit enforcement, or notebook payload generation. Please add a focused
tests/integration/case for/flights/uploadand/flights/{id}/notebookalongside these unit checks.As per coding guidelines, "Place pure-function tests in
tests/unit/and API or database flows intests/integration/."🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/unit/test_routes/test_flights_route.py` around lines 545 - 656, Add real integration tests under tests/integration that exercise the actual FastAPI app (not mocked controllers) for the two endpoints: POST /flights/upload and GET /flights/{id}/notebook. For /flights/upload create a test (e.g., test_integration_import_flight_from_rpy) that posts a real multipart file with valid .rpy content and asserts a 201, verifies response body contains flight_id/rocket_id/motor_id/environment_id and that large payloads (>10MB) return 413 and do not create a flight; for /flights/{id}/notebook create a test (e.g., test_integration_get_flight_notebook) that requests an existing flight id, asserts 200, content-type application/x-ipynb+json and that returned JSON has nbformat and cells, and also cover 404 for missing ids; use the real app instance/TestClient and any real persistence or test DB fixtures your suite provides so multipart parsing, upload-limit enforcement and notebook generation are actually exercised.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/services/flight.py`:
- Around line 93-95: After json.loads(content) returns into data, ensure data is
a JSON object (dict) before calling data.get(): validate with isinstance(data,
dict) and if it is not an object raise the same 422/ValueError path used for
malformed .rpy uploads (or convert to the existing BadRequest/ValidationError
used by this service) so that the code around data = json.loads(content),
simulation = data.get("simulation", data) does not trigger an AttributeError for
inputs like [] or "foo"; adjust subsequent logic that reads simulation
accordingly.
- Around line 482-487: The .rpy export currently omits computed outputs
(json.dumps call using include_outputs=False) causing deserialized Flight
objects to lack outputs when later loaded without resimulation; fix by either
changing the export to include_outputs=True in the json.dumps call that returns
{"simulation": self.flight} (so RocketPyEncoder writes computed outputs), or
update the notebook/load routine that calls the Flight deserializer to use
resimulate=True so outputs are recomputed on load (ensure the loader that reads
the .rpy sets resimulate flag accordingly).
---
Nitpick comments:
In `@tests/unit/test_routes/test_flights_route.py`:
- Around line 545-656: Add real integration tests under tests/integration that
exercise the actual FastAPI app (not mocked controllers) for the two endpoints:
POST /flights/upload and GET /flights/{id}/notebook. For /flights/upload create
a test (e.g., test_integration_import_flight_from_rpy) that posts a real
multipart file with valid .rpy content and asserts a 201, verifies response body
contains flight_id/rocket_id/motor_id/environment_id and that large payloads
(>10MB) return 413 and do not create a flight; for /flights/{id}/notebook create
a test (e.g., test_integration_get_flight_notebook) that requests an existing
flight id, asserts 200, content-type application/x-ipynb+json and that returned
JSON has nbformat and cells, and also cover 404 for missing ids; use the real
app instance/TestClient and any real persistence or test DB fixtures your suite
provides so multipart parsing, upload-limit enforcement and notebook generation
are actually exercised.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: aef613c2-c97d-4e4e-a207-d22009ef29a6
📒 Files selected for processing (3)
src/routes/flight.pysrc/services/flight.pytests/unit/test_routes/test_flights_route.py
- Guard from_rpy against non-dict JSON payloads (e.g. [] or "foo") that would AttributeError instead of hitting the 422 path - Change notebook to resimulate=True so flight.all_info() has computed outputs available after loading an .rpy with include_outputs=False Made-with: Cursor
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/services/flight.py`:
- Around line 286-288: The two tank fields initial_liquid_mass and
initial_gas_mass are not being converted with the utility _f() like the other
tank-specific values, which can pass Function objects into MotorTank and cause
validation/serialization errors; update the creation of the tank dict (where
tank is used and passed to MotorTank) to wrap tank.initial_liquid_mass and
tank.initial_gas_mass with _f(), matching the pattern used for other fields, so
MotorTank receives numeric/serializable values.
Consistent with all other tank-kind fields — these attributes can be Function objects in RocketPy, so they need the same scalar extraction to avoid Pydantic validation failures. Made-with: Cursor
Summary
.rpyfiles instead of architecture-pinned dill binaries.POST /flights/uploadaccepts a.rpyJSON file, decomposes the Flight into Environment, Motor, Rocket, and Flight models, persists each via the standard CRUD pipeline, and returns all IDs.GET /flights/{id}/rocketpynow exports usingRocketPyEncoder(JSON) instead ofdill.dumps.GET /flights/{id}/notebookreturns a.ipynbthat loads the.rpyfile withrocketpy.utilities.load_from_rpyand callsflight.all_info().isinstancechains in extraction helpers with Python 3.10+ structural pattern matching (match/case), consolidated duplicated solid/hybrid grain fields, and used dict merge (|=) and comprehension patterns.Changes
src/services/flight.pyfrom_binary()→from_rpy()(JSON viaRocketPyDecoder),get_flight_binary()→get_flight_rpy()(JSON viaRocketPyEncoder), newgenerate_notebook(),dillimport removed, extraction helpers refactored tomatch/casesrc/controllers/flight.pyimport_flight_from_rpy(),get_flight_notebook(),_persist_model()helpersrc/routes/flight.pyapplication/octet-stream→application/json, filename.dill→.rpy, newPOST /flights/uploadandGET /flights/{id}/notebookroutessrc/views/flight.pyFlightImportedresponse modelrequirements.txtpython-multipart(file uploads)tests/unit/test_routes/test_flights_route.py.rpy/JSON, added import and notebook test suitesTest plan
.rpyfile viaPOST /flights/uploadand verify IDs returned point to valid persisted modelsGET /flights/{id}/rocketpyreturns valid.rpyJSON that round-trips throughRocketPyDecoderGET /flights/{id}/notebookreturns a working.ipynbthat loads the.rpyand runsall_info()Made with Cursor
Summary by CodeRabbit
New Features
Views
Tests
Chores