X Tutup
Skip to content

ENH: portable .rpy flight import/export and notebook generation#70

Open
GabrielBarberini wants to merge 6 commits intomasterfrom
feat/flight-binary-and-notebook-export
Open

ENH: portable .rpy flight import/export and notebook generation#70
GabrielBarberini wants to merge 6 commits intomasterfrom
feat/flight-binary-and-notebook-export

Conversation

@GabrielBarberini
Copy link
Collaborator

@GabrielBarberini GabrielBarberini commented Mar 1, 2026

Summary

  • Closes Load and Save rocketpy flights as binaries #56 — Load and save RocketPy flights as portable .rpy files instead of architecture-pinned dill binaries. POST /flights/upload accepts a .rpy JSON 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}/rocketpy now exports using RocketPyEncoder (JSON) instead of dill.dumps.
  • Closes Export flight as python notebook #57 — Export flight as a Jupyter notebook. GET /flights/{id}/notebook returns a .ipynb that loads the .rpy file with rocketpy.utilities.load_from_rpy and calls flight.all_info().
  • Replaced all isinstance chains 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

File What changed
src/services/flight.py from_binary()from_rpy() (JSON via RocketPyDecoder), get_flight_binary()get_flight_rpy() (JSON via RocketPyEncoder), new generate_notebook(), dill import removed, extraction helpers refactored to match/case
src/controllers/flight.py Renamed methods to match service, added import_flight_from_rpy(), get_flight_notebook(), _persist_model() helper
src/routes/flight.py Export media type application/octet-streamapplication/json, filename .dill.rpy, new POST /flights/upload and GET /flights/{id}/notebook routes
src/views/flight.py Added FlightImported response model
requirements.txt Added python-multipart (file uploads)
tests/unit/test_routes/test_flights_route.py Updated existing binary tests for .rpy/JSON, added import and notebook test suites

Test plan

  • All 155 unit tests pass
  • Pylint 10/10, Flake8 clean, Black + Ruff formatted
  • Manual: upload a real .rpy file via POST /flights/upload and verify IDs returned point to valid persisted models
  • Manual: GET /flights/{id}/rocketpy returns valid .rpy JSON that round-trips through RocketPyDecoder
  • Manual: GET /flights/{id}/notebook returns a working .ipynb that loads the .rpy and runs all_info()

Made with Cursor

Summary by CodeRabbit

  • New Features

    • Export flights as portable .rpy JSON (replaces prior binary), import .rpy JSON (10 MB limit) returning structured import result, and generate/download flight Jupyter notebooks (.ipynb); new endpoints and response content-types added.
  • Views

    • Added FlightImported response view for import results.
  • Tests

    • Updated and expanded tests for .rpy export/import, notebook generation, and related fixtures/imports.
  • Chores

    • Added python-multipart dependency for file uploads.

…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
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 1, 2026

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds .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 python-multipart added to requirements.

Changes

Cohort / File(s) Summary
Dependencies
requirements.txt
Added python-multipart to support multipart file uploads.
Flight Service Serialization
src/services/flight.py
Added from_rpy() (JSON/.rpy deserialization), get_flight_rpy() (JSON serialization), generate_notebook(), extract_models() and multiple extraction helpers; added flight setter.
Flight Controller
src/controllers/flight.py
Renamed get_rocketpy_flight_binaryget_rocketpy_flight_rpy; added _persist_model(), import_flight_from_rpy(), and get_flight_notebook(); updated imports to expose FlightImported and MotorModel.
Flight Routes
src/routes/flight.py
Added endpoints: get_rocketpy_flight_rpy (exports .rpy JSON), import_flight_from_rpy (multipart upload with 10MB limit, returns FlightImported), and get_flight_notebook (exports .ipynb JSON). Removed previous binary export route and updated MIME/Content-Disposition handling.
Flight Views
src/views/flight.py
Added FlightImported view model with flight_id, rocket_id, motor_id, and environment_id for import responses.
Dependency Annotations
src/dependencies.py
Reformatted RocketControllerDep and FlightControllerDep Annotated declarations to multiline (no behavioral change).
Formatting & Minor
src/routes/rocket.py, src/services/rocket.py
Minor whitespace/formatting edits; no functional changes.
Tests
tests/unit/test_routes/*
Updated flight route tests for .rpy JSON workflow and notebook export; added import tests and size-limit tests; removed unused Mock imports; added cache clearing in environment controller fixture.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • Gui-FernandesBR
  • aasitvora99
  • luimot

Poem

🐰 I nibble bytes and stitch JSON seams,
Flights sprout wings from curious dreams.
Notebooks bloom with runnable delight,
.rpy tucked safe for morning flight.
Hop — the import lands just right.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title accurately summarizes the main feature: adding portable .rpy flight import/export and Jupyter notebook generation functionality.
Linked Issues check ✅ Passed All primary objectives from linked issues are met: Issue #56 requirements for import/export using RocketPy utilities [#56] and persisting flights are implemented via .rpy JSON format; Issue #57 requirement for Jupyter notebook export [#57] is implemented with flight.all_info() execution.
Out of Scope Changes check ✅ Passed All code changes are directly related to the stated objectives. Minor changes include formatting (dependencies.py, rocket.py), test hygiene (cache_clear additions), and import cleanup across test files, which support the core functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/flight-binary-and-notebook-export

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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 | 🟡 Minor

Missing cache_clear() call before yield in fixture.

The mock_controller_instance fixture only calls get_environment_controller.cache_clear() after yielding, but not before. Other test files in this PR (e.g., test_motors_route.py and test_rockets_route.py) call cache_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: Prefer TypeError for type validation failure.

The static analysis correctly identifies that TypeError is more appropriate than ValueError when 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_fins with n=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

📥 Commits

Reviewing files that changed from the base of the PR and between 9067812 and 50be27f.

📒 Files selected for processing (12)
  • requirements.txt
  • src/controllers/flight.py
  • src/dependencies.py
  • src/routes/flight.py
  • src/routes/rocket.py
  • src/services/flight.py
  • src/services/rocket.py
  • src/views/flight.py
  • tests/unit/test_routes/test_environments_route.py
  • tests/unit/test_routes/test_flights_route.py
  • tests/unit/test_routes/test_motors_route.py
  • tests/unit/test_routes/test_rockets_route.py

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 .rpy JSON (RocketPyEncoder/RocketPyDecoder) and add .rpy upload/import flow.
  • Add GET /flights/{id}/notebook to return a generated .ipynb that loads the exported .rpy and calls flight.all_info().
  • Update routes/views/tests and add python-multipart to 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.version to 3.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 from sys.version_info at generation time.
                "language_info": {
                    "name": "python",
                    "version": "3.12.0",
                },

src/views/flight.py:156

  • FlightImported.message says the flight was imported "from binary", but this PR switches imports to JSON-based .rpy files. 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_rpy declares controller: FlightControllerDep = None, unlike the other routes in this module which rely on dependency injection without a default. Dropping the = None keeps 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 .rpy uploads 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
@GabrielBarberini
Copy link
Collaborator Author

All actionable review comments from CodeRabbit and Copilot have been addressed in ee13c2f:

Comment Fix
Upload size guard (CodeRabbit + Copilot) Added MAX_RPY_UPLOAD_BYTES = 10 MB cap with HTTP 413 early-exit in import_flight_from_rpy
Implicit string concatenation (CodeRabbit — Pylint CI failure) Merged "from rocketpy.utilities import " "load_from_rpy\n" into a single string
FlightImported.message says "binary" (CodeRabbit + Copilot) Changed to "Flight successfully imported from .rpy file"
Tanks not populated for LIQUID/HYBRID (Copilot) Added _extract_tanks() and _to_float() helpers that extract geometry, fluids, and kind-specific fields from live RocketPy tank objects
Default fins fallback undocumented (CodeRabbit) Added inline comment explaining the n=0 schema-satisfying placeholder
Loose test assertion on message (CodeRabbit) Tightened to exact string match
Missing pre-yield cache_clear() in env test (CodeRabbit) Added to match other test files

Intentionally not addressed:

  • TypeError vs ValueErrorValueError is correct here; we're validating the content of the deserialized payload, not a type mismatch in a function signature.
  • Partial failure / transaction handling — Out of scope for this PR; the sequential persist is acceptable for MVP and documented as a known limitation.
  • controller: FlightControllerDep = None — Kept as-is; the = None is needed because File(...) is a non-default argument and FastAPI requires default-bearing params to come after it. Removing it would break the endpoint.

All 155 tests pass, Pylint 10/10, Flake8 clean.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
tests/unit/test_routes/test_flights_route.py (1)

545-602: Add a test for oversized .rpy uploads (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

📥 Commits

Reviewing files that changed from the base of the PR and between 50be27f and ee13c2f.

📒 Files selected for processing (5)
  • src/routes/flight.py
  • src/services/flight.py
  • src/views/flight.py
  • tests/unit/test_routes/test_environments_route.py
  • tests/unit/test_routes/test_flights_route.py

Gui-FernandesBR
Gui-FernandesBR previously approved these changes Mar 8, 2026
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
@GabrielBarberini
Copy link
Collaborator Author

All remaining review comments addressed in 5eb763f:

Comment Fix
Add HTTP 413 to route response schema (CodeRabbit) Added 413: {"description": "Uploaded .rpy file exceeds size limit"} to the /upload responses decorator
Eager grain-field access crashes non-grain motors (CodeRabbit — critical) Moved grain_fields construction inside the `SOLID
verbose dropped during flight extraction (CodeRabbit) Already fixed in 5d291c7
Add test for oversized upload (HTTP 413) (CodeRabbit — nitpick) Added test_import_flight_from_rpy_payload_too_large

All 156 tests pass, Pylint 10/10, Flake8 clean.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
src/services/flight.py (1)

179-197: ⚠️ Potential issue | 🔴 Critical

Unfixed critical bug: grain_fields built 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 raise AttributeError for LiquidMotor and GenericMotor instances that lack these attributes.

Move grain_fields construction inside the SOLID | HYBRID branch:

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 catching JSONDecodeError for consistent error reporting.

If the input bytes are not valid JSON, json.loads(content) at line 93 will raise JSONDecodeError. Consider wrapping in a try/except to provide a consistent ValueError with 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.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f63d627d-a8b2-40d3-8a41-03b22bb44b71

📥 Commits

Reviewing files that changed from the base of the PR and between ee13c2f and 5d291c7.

📒 Files selected for processing (1)
  • src/services/flight.py

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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/upload and /flights/{id}/notebook alongside these unit checks.

As per coding guidelines, "Place pure-function tests in tests/unit/ and API or database flows in tests/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

📥 Commits

Reviewing files that changed from the base of the PR and between 5d291c7 and 5eb763f.

📒 Files selected for processing (3)
  • src/routes/flight.py
  • src/services/flight.py
  • tests/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
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

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.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1adc3097-0968-444e-9a85-818f2c62148e

📥 Commits

Reviewing files that changed from the base of the PR and between 5eb763f and 525fa96.

📒 Files selected for processing (1)
  • src/services/flight.py

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Export flight as python notebook Load and Save rocketpy flights as binaries

3 participants

X Tutup