22Flight 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+ )
615from opentelemetry import trace
716
817from src .views .flight import (
918 FlightSimulation ,
1019 FlightCreated ,
1120 FlightRetrieved ,
21+ FlightImported ,
1222)
1323from src .models .environment import EnvironmentModel
1424from src .models .flight import FlightModel , FlightWithReferencesRequest
2737
2838tracer = trace .get_tracer (__name__ )
2939
40+ MAX_RPY_UPLOAD_BYTES = 10 * 1024 * 1024 # 10 MB
41+
3042
3143@router .post ("/" , status_code = 201 )
3244async def create_flight (
@@ -76,6 +88,7 @@ async def read_flight(
7688 with tracer .start_as_current_span ("read_flight" ):
7789 return await controller .get_flight_by_id (flight_id )
7890
91+
7992@router .put ("/{flight_id}" , status_code = 204 )
8093async def update_flight (
8194 flight_id : str ,
@@ -117,6 +130,7 @@ async def update_flight_from_references(
117130 flight_id , payload
118131 )
119132
133+
120134@router .delete ("/{flight_id}" , status_code = 204 )
121135async def delete_flight (
122136 flight_id : str ,
@@ -136,34 +150,110 @@ async def delete_flight(
136150 "/{flight_id}/rocketpy" ,
137151 responses = {
138152 200 : {
139- "description" : "Binary file download" ,
140- "content" : {"application/octet-stream " : {}},
153+ "description" : "Portable .rpy JSON file download" ,
154+ "content" : {"application/json " : {}},
141155 }
142156 },
143157 status_code = 200 ,
144158 response_class = Response ,
145159)
160+ async def get_rocketpy_flight_rpy (
161+ flight_id : str ,
162+ controller : FlightControllerDep ,
163+ ):
164+ """
165+ Export a rocketpy Flight as a portable ``.rpy`` JSON file.
166+
167+ The ``.rpy`` format is architecture-, OS-, and
168+ Python-version-agnostic.
169+
170+ ## Args
171+ ``` flight_id: str ```
172+ """
173+ with tracer .start_as_current_span ("get_rocketpy_flight_rpy" ):
174+ headers = {
175+ 'Content-Disposition' : (
176+ 'attachment; filename=' f'"rocketpy_flight_{ flight_id } .rpy"'
177+ ),
178+ }
179+ rpy = await controller .get_rocketpy_flight_rpy (flight_id )
180+ return Response (
181+ content = rpy ,
182+ headers = headers ,
183+ media_type = "application/json" ,
184+ status_code = 200 ,
185+ )
186+
187+
188+ @router .post (
189+ "/upload" ,
190+ status_code = 201 ,
191+ responses = {
192+ 201 : {"description" : "Flight imported from .rpy file" },
193+ 413 : {"description" : "Uploaded .rpy file exceeds size limit" },
194+ 422 : {"description" : "Invalid .rpy file" },
195+ },
196+ )
197+ async def import_flight_from_rpy (
198+ file : UploadFile = File (...),
199+ controller : FlightControllerDep = None , # noqa: B008
200+ ) -> FlightImported :
201+ """
202+ Upload a ``.rpy`` JSON file containing a RocketPy Flight.
203+
204+ The file is deserialized and decomposed into its
205+ constituent objects (Environment, Motor, Rocket, Flight).
206+ Each object is persisted as a normal JSON model and the
207+ corresponding IDs are returned. Maximum upload size is 10 MB.
208+
209+ ## Args
210+ ``` file: .rpy JSON upload ```
211+ """
212+ with tracer .start_as_current_span ("import_flight_from_rpy" ):
213+ content = await file .read (MAX_RPY_UPLOAD_BYTES + 1 )
214+ if len (content ) > MAX_RPY_UPLOAD_BYTES :
215+ raise HTTPException (
216+ status_code = status .HTTP_413_REQUEST_ENTITY_TOO_LARGE ,
217+ detail = "Uploaded .rpy file exceeds 10 MB limit." ,
218+ )
219+ return await controller .import_flight_from_rpy (content )
220+
146221
147- async def get_rocketpy_flight_binary (
222+ @router .get (
223+ "/{flight_id}/notebook" ,
224+ responses = {
225+ 200 : {
226+ "description" : "Jupyter notebook file download" ,
227+ "content" : {"application/x-ipynb+json" : {}},
228+ }
229+ },
230+ status_code = 200 ,
231+ response_class = Response ,
232+ )
233+ async def get_flight_notebook (
148234 flight_id : str ,
149235 controller : FlightControllerDep ,
150236):
151237 """
152- Loads rocketpy.flight as a dill binary.
153- Currently only amd64 architecture is supported.
238+ Export a flight as a Jupyter notebook (.ipynb).
239+
240+ The notebook loads the flight's ``.rpy`` file and calls
241+ ``flight.all_info()`` for interactive exploration.
154242
155243 ## Args
156244 ``` flight_id: str ```
157245 """
158- with tracer .start_as_current_span ("get_rocketpy_flight_binary" ):
246+ with tracer .start_as_current_span ("get_flight_notebook" ):
247+ notebook = await controller .get_flight_notebook (flight_id )
248+ content = json .dumps (notebook , indent = 1 ).encode ()
249+ filename = f"flight_{ flight_id } .ipynb"
159250 headers = {
160- ' Content-Disposition' : f'attachment; filename="rocketpy_flight_ { flight_id } .dill"'
251+ " Content-Disposition" : ( f'attachment; filename="{ filename } "' ),
161252 }
162- binary = await controller .get_rocketpy_flight_binary (flight_id )
163253 return Response (
164- content = binary ,
254+ content = content ,
165255 headers = headers ,
166- media_type = "application/octet-stream " ,
256+ media_type = "application/x-ipynb+json " ,
167257 status_code = 200 ,
168258 )
169259
@@ -210,6 +300,7 @@ async def update_flight_rocket(
210300 rocket = rocket ,
211301 )
212302
303+
213304@router .get ("/{flight_id}/simulate" )
214305async def get_flight_simulation (
215306 flight_id : str ,
@@ -222,4 +313,4 @@ async def get_flight_simulation(
222313 ``` flight_id: Flight ID ```
223314 """
224315 with tracer .start_as_current_span ("get_flight_simulation" ):
225- return await controller .get_flight_simulation (flight_id )
316+ return await controller .get_flight_simulation (flight_id )
0 commit comments