Skip to content

Commit 058f62a

Browse files
committed
Merge remote-tracking branch 'origin/feat/flight-binary-and-notebook-export' into drop-22apr
2 parents edf06e5 + 3882575 commit 058f62a

7 files changed

Lines changed: 737 additions & 33 deletions

File tree

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
dill
22
python-dotenv
3+
python-multipart
34
fastapi
45
uvloop
56
pydantic

src/controllers/flight.py

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
ControllerBase,
55
controller_exception_handler,
66
)
7-
from src.views.flight import FlightSimulation, FlightCreated
7+
from src.views.flight import FlightSimulation, FlightCreated, FlightImported
88
from src.models.flight import (
99
FlightModel,
1010
FlightWithReferencesRequest,
1111
)
1212
from src.models.environment import EnvironmentModel
13+
from src.models.motor import MotorModel
1314
from src.models.rocket import RocketModel
1415
from src.repositories.interface import RepositoryInterface
1516
from src.services.flight import FlightService
@@ -22,6 +23,7 @@ class FlightController(ControllerBase):
2223
Enables:
2324
- Simulation of a RocketPy Flight.
2425
- CRUD for Flight BaseApiModel.
26+
- Import/export as portable .rpy files and Jupyter notebooks.
2527
"""
2628

2729
def __init__(self):
@@ -122,25 +124,26 @@ async def update_rocket_by_flight_id(
122124
return
123125

124126
@controller_exception_handler
125-
async def get_rocketpy_flight_binary(
127+
async def get_rocketpy_flight_rpy(
126128
self,
127129
flight_id: str,
128130
) -> bytes:
129131
"""
130-
Get rocketpy.flight as dill binary.
132+
Get rocketpy.flight as a portable ``.rpy`` JSON file.
131133
132134
Args:
133135
flight_id: str
134136
135137
Returns:
136-
bytes
138+
bytes (UTF-8 encoded JSON)
137139
138140
Raises:
139-
HTTP 404 Not Found: If the flight is not found in the database.
141+
HTTP 404 Not Found: If the flight is not found
142+
in the database.
140143
"""
141144
flight = await self.get_flight_by_id(flight_id)
142145
flight_service = FlightService.from_flight_model(flight.flight)
143-
return flight_service.get_flight_binary()
146+
return flight_service.get_flight_rpy()
144147

145148
@controller_exception_handler
146149
async def get_flight_simulation(
@@ -162,3 +165,71 @@ async def get_flight_simulation(
162165
flight = await self.get_flight_by_id(flight_id)
163166
flight_service = FlightService.from_flight_model(flight.flight)
164167
return flight_service.get_flight_simulation()
168+
169+
async def _persist_model(self, model_cls, model_instance) -> str:
170+
repo_cls = RepositoryInterface.get_model_repo(model_cls)
171+
async with repo_cls() as repo:
172+
creator = getattr(repo, f"create_{model_cls.NAME}")
173+
return await creator(model_instance)
174+
175+
@controller_exception_handler
176+
async def import_flight_from_rpy(
177+
self,
178+
content: bytes,
179+
) -> FlightImported:
180+
"""
181+
Import a ``.rpy`` JSON file: decompose the RocketPy Flight
182+
into Environment, Motor, Rocket and Flight models, persist
183+
each one via the normal CRUD pipeline, and return all IDs.
184+
185+
Args:
186+
content: raw bytes of a ``.rpy`` JSON file.
187+
188+
Returns:
189+
FlightImported with environment_id, motor_id,
190+
rocket_id, and flight_id.
191+
192+
Raises:
193+
HTTP 422: If the file is not a valid ``.rpy`` Flight.
194+
"""
195+
try:
196+
flight_service = FlightService.from_rpy(content)
197+
except Exception as exc:
198+
raise HTTPException(
199+
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
200+
detail=f"Invalid .rpy file: {exc}",
201+
) from exc
202+
203+
env, motor, rocket, flight = flight_service.extract_models()
204+
205+
env_id = await self._persist_model(EnvironmentModel, env)
206+
motor_id = await self._persist_model(MotorModel, motor)
207+
rocket_id = await self._persist_model(RocketModel, rocket)
208+
flight_id = await self._persist_model(FlightModel, flight)
209+
210+
return FlightImported(
211+
flight_id=flight_id,
212+
rocket_id=rocket_id,
213+
motor_id=motor_id,
214+
environment_id=env_id,
215+
)
216+
217+
@controller_exception_handler
218+
async def get_flight_notebook(
219+
self,
220+
flight_id: str,
221+
) -> dict:
222+
"""
223+
Generate a Jupyter notebook for a persisted flight.
224+
225+
Args:
226+
flight_id: str
227+
228+
Returns:
229+
dict representing a valid .ipynb.
230+
231+
Raises:
232+
HTTP 404 Not Found: If the flight does not exist.
233+
"""
234+
await self.get_flight_by_id(flight_id)
235+
return FlightService.generate_notebook(flight_id)

src/routes/flight.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,23 @@
22
Flight routes with dependency injection for improved performance.
33
"""
44

5-
from fastapi import APIRouter, Response
5+
import json
6+
7+
from fastapi import (
8+
APIRouter,
9+
File,
10+
HTTPException,
11+
Response,
12+
UploadFile,
13+
status,
14+
)
615
from opentelemetry import trace
716

817
from src.views.flight import (
918
FlightSimulation,
1019
FlightCreated,
1120
FlightRetrieved,
21+
FlightImported,
1222
)
1323
from src.models.environment import EnvironmentModel
1424
from src.models.flight import FlightModel, FlightWithReferencesRequest
@@ -27,6 +37,8 @@
2737

2838
tracer = trace.get_tracer(__name__)
2939

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

3143
@router.post("/", status_code=201)
3244
async def create_flight(
@@ -77,6 +89,7 @@ async def read_flight(
7789
return await controller.get_flight_by_id(flight_id)
7890

7991

92+
8093
@router.put("/{flight_id}", status_code=204)
8194
async def update_flight(
8295
flight_id: str,
@@ -138,33 +151,38 @@ async def delete_flight(
138151
"/{flight_id}/rocketpy",
139152
responses={
140153
200: {
141-
"description": "Binary file download",
142-
"content": {"application/octet-stream": {}},
154+
"description": "Portable .rpy JSON file download",
155+
"content": {"application/json": {}},
143156
}
144157
},
145158
status_code=200,
146159
response_class=Response,
147160
)
161+
148162
async def get_rocketpy_flight_binary(
149163
flight_id: str,
150164
controller: FlightControllerDep,
151165
):
152166
"""
153-
Loads rocketpy.flight as a dill binary.
154-
Currently only amd64 architecture is supported.
167+
Export a flight as a Jupyter notebook (.ipynb).
168+
169+
The notebook loads the flight's ``.rpy`` file and calls
170+
``flight.all_info()`` for interactive exploration.
155171
156172
## Args
157173
``` flight_id: str ```
158174
"""
159-
with tracer.start_as_current_span("get_rocketpy_flight_binary"):
175+
with tracer.start_as_current_span("get_flight_notebook"):
176+
notebook = await controller.get_flight_notebook(flight_id)
177+
content = json.dumps(notebook, indent=1).encode()
178+
filename = f"flight_{flight_id}.ipynb"
160179
headers = {
161-
'Content-Disposition': f'attachment; filename="rocketpy_flight_{flight_id}.dill"'
180+
"Content-Disposition": (f'attachment; filename="{filename}"'),
162181
}
163-
binary = await controller.get_rocketpy_flight_binary(flight_id)
164182
return Response(
165-
content=binary,
183+
content=content,
166184
headers=headers,
167-
media_type="application/octet-stream",
185+
media_type="application/x-ipynb+json",
168186
status_code=200,
169187
)
170188

0 commit comments

Comments
 (0)