Skip to content
2 changes: 1 addition & 1 deletion .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@ jobs:
pip install -r requirements.txt
- name: Analysing the code with pylint
run: |
pylint -d C0415,C0200,C0301,C0114,R0903,C0115,W0246,R0914,C0209,E1121,C0103,C2801,R0801,E1101,E0401,E0611,R0911,C0116,W0212,W0719,W0601,W1203,W0123,W0511,W0621,R0913,R0917 $(git ls-files '*.py')
pylint -d R0912,C0415,C0200,C0301,C0114,R0903,C0115,W0246,R0914,C0209,E1121,C0103,C2801,R0801,E1101,E0401,E0611,R0911,C0116,W0212,W0719,W0601,W1203,W0123,W0511,W0621,R0913,R0917 $(git ls-files '*.py')
60 changes: 58 additions & 2 deletions src/controllers/flight.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
from fastapi import HTTPException, status

from src.controllers.interface import (
ControllerBase,
controller_exception_handler,
)
from src.views.flight import FlightSimulation
from src.models.flight import FlightModel
from src.views.flight import FlightSimulation, FlightCreated
from src.models.flight import (
FlightModel,
FlightWithReferencesRequest,
)
from src.models.environment import EnvironmentModel
from src.models.rocket import RocketModel
from src.repositories.interface import RepositoryInterface
from src.services.flight import FlightService


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

async def _load_environment(self, environment_id: str) -> EnvironmentModel:
repo_cls = RepositoryInterface.get_model_repo(EnvironmentModel)
async with repo_cls() as repo:
environment = await repo.read_environment_by_id(environment_id)
if environment is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Environment not found",
)
return environment

async def _load_rocket(self, rocket_id: str) -> RocketModel:
repo_cls = RepositoryInterface.get_model_repo(RocketModel)
async with repo_cls() as repo:
rocket = await repo.read_rocket_by_id(rocket_id)
if rocket is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Rocket not found",
)
return rocket

@controller_exception_handler
async def create_flight_from_references(
self, payload: FlightWithReferencesRequest
) -> FlightCreated:
environment = await self._load_environment(payload.environment_id)
rocket = await self._load_rocket(payload.rocket_id)
flight_model = payload.flight.assemble(
environment=environment,
rocket=rocket,
)
return await self.post_flight(flight_model)

@controller_exception_handler
async def update_flight_from_references(
self,
flight_id: str,
payload: FlightWithReferencesRequest,
) -> None:
environment = await self._load_environment(payload.environment_id)
rocket = await self._load_rocket(payload.rocket_id)
flight_model = payload.flight.assemble(
environment=environment,
rocket=rocket,
)
flight_model.set_id(flight_id)
await self.put_flight_by_id(flight_id, flight_model)
return

@controller_exception_handler
async def update_environment_by_flight_id(
self, flight_id: str, *, environment: EnvironmentModel
Expand Down
42 changes: 40 additions & 2 deletions src/controllers/rocket.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
from fastapi import HTTPException, status

from src.controllers.interface import (
ControllerBase,
controller_exception_handler,
)
from src.views.rocket import RocketSimulation
from src.models.rocket import RocketModel
from src.views.rocket import RocketSimulation, RocketCreated
from src.models.motor import MotorModel
from src.models.rocket import (
RocketModel,
RocketWithMotorReferenceRequest,
)
from src.repositories.interface import RepositoryInterface
from src.services.rocket import RocketService


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

async def _load_motor(self, motor_id: str) -> MotorModel:
repo_cls = RepositoryInterface.get_model_repo(MotorModel)
async with repo_cls() as repo:
motor = await repo.read_motor_by_id(motor_id)
if motor is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Motor not found",
)
return motor

@controller_exception_handler
async def create_rocket_from_motor_reference(
self, payload: RocketWithMotorReferenceRequest
) -> RocketCreated:
motor = await self._load_motor(payload.motor_id)
rocket_model = payload.rocket.assemble(motor)
return await self.post_rocket(rocket_model)

@controller_exception_handler
async def update_rocket_from_motor_reference(
self,
rocket_id: str,
payload: RocketWithMotorReferenceRequest,
) -> None:
motor = await self._load_motor(payload.motor_id)
rocket_model = payload.rocket.assemble(motor)
rocket_model.set_id(rocket_id)
await self.put_rocket_by_id(rocket_id, rocket_model)
return

@controller_exception_handler
async def get_rocketpy_rocket_binary(self, rocket_id: str) -> bytes:
"""
Expand Down
76 changes: 76 additions & 0 deletions src/models/flight.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import json
from typing import Optional, Self, ClassVar, Literal

from pydantic import BaseModel, Field, field_validator
from src.models.interface import ApiBaseModel
from src.models.rocket import RocketModel
from src.models.environment import EnvironmentModel
Expand Down Expand Up @@ -69,3 +72,76 @@ def RETRIEVED(model_instance: type(Self)):
**model_instance.model_dump(),
)
)

@field_validator('environment', mode='before')
@classmethod
def _coerce_environment(cls, value):
if isinstance(value, str):
try:
return json.loads(value)
except json.JSONDecodeError as exc:
raise ValueError(
'Invalid JSON for environment payload'
) from exc
return value

@field_validator('rocket', mode='before')
@classmethod
def _coerce_rocket(cls, value):
if isinstance(value, str):
try:
return json.loads(value)
except json.JSONDecodeError as exc:
raise ValueError('Invalid JSON for rocket payload') from exc
return value


class FlightPartialModel(BaseModel):
"""Flight attributes required when rocket/environment are referenced."""

name: str = Field(default="flight")
rail_length: float = 1
time_overshoot: bool = True
terminate_on_apogee: bool = False
equations_of_motion: Literal['standard', 'solid_propulsion'] = 'standard'
inclination: float = 90.0
heading: float = 0.0
max_time: Optional[int] = None
max_time_step: Optional[float] = None
min_time_step: Optional[int] = None
rtol: Optional[float] = None
atol: Optional[float] = None
verbose: Optional[bool] = None

def assemble(
self,
*,
environment: EnvironmentModel,
rocket: RocketModel,
) -> FlightModel:
"""Compose a full flight model using referenced resources."""

flight_data = self.model_dump(exclude_none=True)
return FlightModel(
environment=environment,
rocket=rocket,
**flight_data,
)


class FlightWithReferencesRequest(BaseModel):
"""Payload for creating or updating flights via component references."""

environment_id: str
rocket_id: str
flight: FlightPartialModel

@field_validator('flight', mode='before')
@classmethod
def _coerce_flight(cls, value):
if isinstance(value, str):
try:
value = json.loads(value)
except json.JSONDecodeError as exc:
raise ValueError('Invalid JSON for flight payload') from exc
return value
15 changes: 14 additions & 1 deletion src/models/motor.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
from enum import Enum
from typing import Optional, Tuple, List, Union, Self, ClassVar, Literal
from pydantic import model_validator
from pydantic import model_validator, field_validator

from src.models.interface import ApiBaseModel
from src.models.sub.tanks import MotorTank
Expand Down Expand Up @@ -57,6 +58,18 @@ class MotorModel(ApiBaseModel):
] = 'nozzle_to_combustion_chamber'
reshape_thrust_curve: Union[bool, tuple] = False

@field_validator('tanks', mode='before')
@classmethod
def _coerce_tanks(cls, value):
if isinstance(value, str):
try:
value = json.loads(value)
except json.JSONDecodeError as exc:
raise ValueError('Invalid JSON for tanks payload') from exc
if isinstance(value, dict):
value = [value]
return value

@model_validator(mode='after')
# TODO: extend guard to check motor kinds and tank kinds specifics
def validate_motor_kind(self):
Expand Down
98 changes: 98 additions & 0 deletions src/models/rocket.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import json
from typing import Optional, Tuple, List, Union, Self, ClassVar, Literal

from pydantic import BaseModel, Field, field_validator
from src.models.interface import ApiBaseModel
from src.models.motor import MotorModel
from src.models.sub.aerosurfaces import (
Expand All @@ -10,6 +13,15 @@
)


def _maybe_parse_json(value):
if isinstance(value, str):
try:
return json.loads(value)
except json.JSONDecodeError as exc:
raise ValueError('Invalid JSON payload') from exc
return value


class RocketModel(ApiBaseModel):
NAME: ClassVar = "rocket"
METHODS: ClassVar = ("POST", "GET", "PUT", "DELETE")
Expand Down Expand Up @@ -37,6 +49,42 @@ class RocketModel(ApiBaseModel):
rail_buttons: Optional[RailButtons] = None
tail: Optional[Tail] = None

@field_validator('motor', mode='before')
@classmethod
def _coerce_motor(cls, value):
return _maybe_parse_json(value)

@field_validator('nose', mode='before')
@classmethod
def _coerce_nose(cls, value):
return _maybe_parse_json(value)

@field_validator('fins', mode='before')
@classmethod
def _coerce_fins(cls, value):
value = _maybe_parse_json(value)
if isinstance(value, dict):
value = [value]
return value

@field_validator('parachutes', mode='before')
@classmethod
def _coerce_parachutes(cls, value):
value = _maybe_parse_json(value)
if isinstance(value, dict):
value = [value]
return value

@field_validator('rail_buttons', mode='before')
@classmethod
def _coerce_rail_buttons(cls, value):
return _maybe_parse_json(value)

@field_validator('tail', mode='before')
@classmethod
def _coerce_tail(cls, value):
return _maybe_parse_json(value)

@staticmethod
def UPDATED():
return
Expand All @@ -61,3 +109,53 @@ def RETRIEVED(model_instance: type(Self)):
**model_instance.model_dump(),
)
)


class RocketPartialModel(BaseModel):
"""Rocket attributes required when a motor is supplied by reference."""

radius: float
mass: float
motor_position: float
center_of_mass_without_motor: float
inertia: Union[
Tuple[float, float, float],
Tuple[float, float, float, float, float, float],
] = (0, 0, 0)
power_off_drag: List[Tuple[float, float]] = Field(
default_factory=lambda: [(0, 0)]
)
power_on_drag: List[Tuple[float, float]] = Field(
default_factory=lambda: [(0, 0)]
)
coordinate_system_orientation: Literal['tail_to_nose', 'nose_to_tail'] = (
'tail_to_nose'
)
nose: NoseCone
fins: List[Fins]
parachutes: Optional[List[Parachute]] = None
rail_buttons: Optional[RailButtons] = None
tail: Optional[Tail] = None

def assemble(self, motor: MotorModel) -> RocketModel:
"""Compose a full rocket model using the referenced motor."""

rocket_data = self.model_dump(exclude_none=True)
return RocketModel(motor=motor, **rocket_data)


class RocketWithMotorReferenceRequest(BaseModel):
"""Payload for creating or updating rockets via motor reference."""

motor_id: str
rocket: RocketPartialModel

@field_validator('rocket', mode='before')
@classmethod
def _coerce_rocket(cls, value):
if isinstance(value, str):
try:
value = json.loads(value)
except json.JSONDecodeError as exc:
raise ValueError('Invalid JSON for rocket payload') from exc
return value
Loading