Skip to content

Commit fe4c9cf

Browse files
implements create/update by reference
1 parent c7bb348 commit fe4c9cf

File tree

6 files changed

+270
-6
lines changed

6 files changed

+270
-6
lines changed

src/controllers/flight.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
1+
from fastapi import HTTPException, status
2+
13
from src.controllers.interface import (
24
ControllerBase,
35
controller_exception_handler,
46
)
5-
from src.views.flight import FlightSimulation
6-
from src.models.flight import FlightModel
7+
from src.views.flight import FlightSimulation, FlightCreated
8+
from src.models.flight import (
9+
FlightModel,
10+
FlightWithReferencesRequest,
11+
)
712
from src.models.environment import EnvironmentModel
813
from src.models.rocket import RocketModel
14+
from src.repositories.interface import RepositoryInterface
915
from src.services.flight import FlightService
1016

1117

@@ -21,6 +27,56 @@ class FlightController(ControllerBase):
2127
def __init__(self):
2228
super().__init__(models=[FlightModel])
2329

30+
async def _load_environment(self, environment_id: str) -> EnvironmentModel:
31+
repo_cls = RepositoryInterface.get_model_repo(EnvironmentModel)
32+
async with repo_cls() as repo:
33+
environment = await repo.read_environment_by_id(environment_id)
34+
if environment is None:
35+
raise HTTPException(
36+
status_code=status.HTTP_404_NOT_FOUND,
37+
detail="Environment not found",
38+
)
39+
return environment
40+
41+
async def _load_rocket(self, rocket_id: str) -> RocketModel:
42+
repo_cls = RepositoryInterface.get_model_repo(RocketModel)
43+
async with repo_cls() as repo:
44+
rocket = await repo.read_rocket_by_id(rocket_id)
45+
if rocket is None:
46+
raise HTTPException(
47+
status_code=status.HTTP_404_NOT_FOUND,
48+
detail="Rocket not found",
49+
)
50+
return rocket
51+
52+
@controller_exception_handler
53+
async def create_flight_from_references(
54+
self, payload: FlightWithReferencesRequest
55+
) -> FlightCreated:
56+
environment = await self._load_environment(payload.environment_id)
57+
rocket = await self._load_rocket(payload.rocket_id)
58+
flight_model = payload.flight.assemble(
59+
environment=environment,
60+
rocket=rocket,
61+
)
62+
return await self.post_flight(flight_model)
63+
64+
@controller_exception_handler
65+
async def update_flight_from_references(
66+
self,
67+
flight_id: str,
68+
payload: FlightWithReferencesRequest,
69+
) -> None:
70+
environment = await self._load_environment(payload.environment_id)
71+
rocket = await self._load_rocket(payload.rocket_id)
72+
flight_model = payload.flight.assemble(
73+
environment=environment,
74+
rocket=rocket,
75+
)
76+
flight_model.set_id(flight_id)
77+
await self.put_flight_by_id(flight_id, flight_model)
78+
return
79+
2480
@controller_exception_handler
2581
async def update_environment_by_flight_id(
2682
self, flight_id: str, *, environment: EnvironmentModel

src/controllers/rocket.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1+
from fastapi import HTTPException, status
2+
13
from src.controllers.interface import (
24
ControllerBase,
35
controller_exception_handler,
46
)
5-
from src.views.rocket import RocketSimulation
6-
from src.models.rocket import RocketModel
7+
from src.views.rocket import RocketSimulation, RocketCreated
8+
from src.models.motor import MotorModel
9+
from src.models.rocket import (
10+
RocketModel,
11+
RocketWithMotorReferenceRequest,
12+
)
13+
from src.repositories.interface import RepositoryInterface
714
from src.services.rocket import RocketService
815

916

@@ -19,6 +26,37 @@ class RocketController(ControllerBase):
1926
def __init__(self):
2027
super().__init__(models=[RocketModel])
2128

29+
async def _load_motor(self, motor_id: str) -> MotorModel:
30+
repo_cls = RepositoryInterface.get_model_repo(MotorModel)
31+
async with repo_cls() as repo:
32+
motor = await repo.read_motor_by_id(motor_id)
33+
if motor is None:
34+
raise HTTPException(
35+
status_code=status.HTTP_404_NOT_FOUND,
36+
detail="Motor not found",
37+
)
38+
return motor
39+
40+
@controller_exception_handler
41+
async def create_rocket_from_motor_reference(
42+
self, payload: RocketWithMotorReferenceRequest
43+
) -> RocketCreated:
44+
motor = await self._load_motor(payload.motor_id)
45+
rocket_model = payload.rocket.assemble(motor)
46+
return await self.post_rocket(rocket_model)
47+
48+
@controller_exception_handler
49+
async def update_rocket_from_motor_reference(
50+
self,
51+
rocket_id: str,
52+
payload: RocketWithMotorReferenceRequest,
53+
) -> None:
54+
motor = await self._load_motor(payload.motor_id)
55+
rocket_model = payload.rocket.assemble(motor)
56+
rocket_model.set_id(rocket_id)
57+
await self.put_rocket_by_id(rocket_id, rocket_model)
58+
return
59+
2260
@controller_exception_handler
2361
async def get_rocketpy_rocket_binary(self, rocket_id: str) -> bytes:
2462
"""

src/models/flight.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from typing import Optional, Self, ClassVar, Literal
2+
3+
from pydantic import BaseModel, Field
24
from src.models.interface import ApiBaseModel
35
from src.models.rocket import RocketModel
46
from src.models.environment import EnvironmentModel
@@ -69,3 +71,44 @@ def RETRIEVED(model_instance: type(Self)):
6971
**model_instance.model_dump(),
7072
)
7173
)
74+
75+
76+
class FlightPartialModel(BaseModel):
77+
"""Flight attributes required when rocket/environment are referenced."""
78+
79+
name: str = Field(default="flight")
80+
rail_length: float = 1
81+
time_overshoot: bool = True
82+
terminate_on_apogee: bool = False
83+
equations_of_motion: Literal['standard', 'solid_propulsion'] = 'standard'
84+
inclination: float = 90.0
85+
heading: float = 0.0
86+
max_time: Optional[int] = None
87+
max_time_step: Optional[float] = None
88+
min_time_step: Optional[int] = None
89+
rtol: Optional[float] = None
90+
atol: Optional[float] = None
91+
verbose: Optional[bool] = None
92+
93+
def assemble(
94+
self,
95+
*,
96+
environment: EnvironmentModel,
97+
rocket: RocketModel,
98+
) -> FlightModel:
99+
"""Compose a full flight model using referenced resources."""
100+
101+
flight_data = self.model_dump(exclude_none=True)
102+
return FlightModel(
103+
environment=environment,
104+
rocket=rocket,
105+
**flight_data,
106+
)
107+
108+
109+
class FlightWithReferencesRequest(BaseModel):
110+
"""Payload for creating or updating flights via component references."""
111+
112+
environment_id: str
113+
rocket_id: str
114+
flight: FlightPartialModel

src/models/rocket.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from typing import Optional, Tuple, List, Union, Self, ClassVar, Literal
2+
3+
from pydantic import BaseModel, Field
24
from src.models.interface import ApiBaseModel
35
from src.models.motor import MotorModel
46
from src.models.sub.aerosurfaces import (
@@ -61,3 +63,43 @@ def RETRIEVED(model_instance: type(Self)):
6163
**model_instance.model_dump(),
6264
)
6365
)
66+
67+
68+
class RocketPartialModel(BaseModel):
69+
"""Rocket attributes required when a motor is supplied by reference."""
70+
71+
radius: float
72+
mass: float
73+
motor_position: float
74+
center_of_mass_without_motor: float
75+
inertia: Union[
76+
Tuple[float, float, float],
77+
Tuple[float, float, float, float, float, float],
78+
] = (0, 0, 0)
79+
power_off_drag: List[Tuple[float, float]] = Field(
80+
default_factory=lambda: [(0, 0)]
81+
)
82+
power_on_drag: List[Tuple[float, float]] = Field(
83+
default_factory=lambda: [(0, 0)]
84+
)
85+
coordinate_system_orientation: Literal['tail_to_nose', 'nose_to_tail'] = (
86+
'tail_to_nose'
87+
)
88+
nose: NoseCone
89+
fins: List[Fins]
90+
parachutes: Optional[List[Parachute]] = None
91+
rail_buttons: Optional[RailButtons] = None
92+
tail: Optional[Tail] = None
93+
94+
def assemble(self, motor: MotorModel) -> RocketModel:
95+
"""Compose a full rocket model using the referenced motor."""
96+
97+
rocket_data = self.model_dump(exclude_none=True)
98+
return RocketModel(motor=motor, **rocket_data)
99+
100+
101+
class RocketWithMotorReferenceRequest(BaseModel):
102+
"""Payload for creating or updating rockets via motor reference."""
103+
104+
motor_id: str
105+
rocket: RocketPartialModel

src/routes/flight.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
FlightRetrieved,
1212
)
1313
from src.models.environment import EnvironmentModel
14-
from src.models.flight import FlightModel
14+
from src.models.flight import FlightModel, FlightWithReferencesRequest
1515
from src.models.rocket import RocketModel
1616
from src.controllers.flight import FlightController
1717

@@ -41,6 +41,25 @@ async def create_flight(flight: FlightModel) -> FlightCreated:
4141
return await controller.post_flight(flight)
4242

4343

44+
@router.post("/from-references", status_code=201)
45+
async def create_flight_from_references(
46+
payload: FlightWithReferencesRequest,
47+
) -> FlightCreated:
48+
"""
49+
Creates a flight using existing rocket and environment references.
50+
51+
## Args
52+
```
53+
environment_id: str
54+
rocket_id: str
55+
flight: Flight-only fields JSON
56+
```
57+
"""
58+
with tracer.start_as_current_span("create_flight_from_references"):
59+
controller = FlightController()
60+
return await controller.create_flight_from_references(payload)
61+
62+
4463
@router.get("/{flight_id}")
4564
async def read_flight(flight_id: str) -> FlightRetrieved:
4665
"""
@@ -70,6 +89,29 @@ async def update_flight(flight_id: str, flight: FlightModel) -> None:
7089
return await controller.put_flight_by_id(flight_id, flight)
7190

7291

92+
@router.put("/{flight_id}/from-references", status_code=204)
93+
async def update_flight_from_references(
94+
flight_id: str,
95+
payload: FlightWithReferencesRequest,
96+
) -> None:
97+
"""
98+
Updates a flight using existing rocket and environment references.
99+
100+
## Args
101+
```
102+
flight_id: str
103+
environment_id: str
104+
rocket_id: str
105+
flight: Flight-only fields JSON
106+
```
107+
"""
108+
with tracer.start_as_current_span("update_flight_from_references"):
109+
controller = FlightController()
110+
return await controller.update_flight_from_references(
111+
flight_id, payload
112+
)
113+
114+
73115
@router.delete("/{flight_id}", status_code=204)
74116
async def delete_flight(flight_id: str) -> None:
75117
"""

src/routes/rocket.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@
1010
RocketCreated,
1111
RocketRetrieved,
1212
)
13-
from src.models.rocket import RocketModel
13+
from src.models.rocket import (
14+
RocketModel,
15+
RocketWithMotorReferenceRequest,
16+
)
1417
from src.controllers.rocket import RocketController
1518

1619
router = APIRouter(
@@ -39,6 +42,24 @@ async def create_rocket(rocket: RocketModel) -> RocketCreated:
3942
return await controller.post_rocket(rocket)
4043

4144

45+
@router.post("/from-motor-reference", status_code=201)
46+
async def create_rocket_from_motor_reference(
47+
payload: RocketWithMotorReferenceRequest,
48+
) -> RocketCreated:
49+
"""
50+
Creates a rocket using an existing motor reference.
51+
52+
## Args
53+
```
54+
motor_id: str
55+
rocket: Rocket-only fields JSON
56+
```
57+
"""
58+
with tracer.start_as_current_span("create_rocket_from_motor_reference"):
59+
controller = RocketController()
60+
return await controller.create_rocket_from_motor_reference(payload)
61+
62+
4263
@router.get("/{rocket_id}")
4364
async def read_rocket(rocket_id: str) -> RocketRetrieved:
4465
"""
@@ -68,6 +89,28 @@ async def update_rocket(rocket_id: str, rocket: RocketModel) -> None:
6889
return await controller.put_rocket_by_id(rocket_id, rocket)
6990

7091

92+
@router.put("/{rocket_id}/from-motor-reference", status_code=204)
93+
async def update_rocket_from_motor_reference(
94+
rocket_id: str,
95+
payload: RocketWithMotorReferenceRequest,
96+
) -> None:
97+
"""
98+
Updates a rocket using an existing motor reference.
99+
100+
## Args
101+
```
102+
rocket_id: str
103+
motor_id: str
104+
rocket: Rocket-only fields JSON
105+
```
106+
"""
107+
with tracer.start_as_current_span("update_rocket_from_motor_reference"):
108+
controller = RocketController()
109+
return await controller.update_rocket_from_motor_reference(
110+
rocket_id, payload
111+
)
112+
113+
71114
@router.delete("/{rocket_id}", status_code=204)
72115
async def delete_rocket(rocket_id: str) -> None:
73116
"""

0 commit comments

Comments
 (0)