Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,10 @@ cython_debug/
#.idea/

# VSCode config
.vscode/
.vscode/

# context specific ignores
.context

# lint
.pylint.d/
22 changes: 22 additions & 0 deletions src/controllers/flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,28 @@ async def get_rocketpy_flight_rpy(
flight_service = FlightService.from_flight_model(flight.flight)
return flight_service.get_flight_rpy()

@controller_exception_handler
async def get_flight_kml(
self,
flight_id: str,
) -> bytes:
"""
Get the flight trajectory as a KML file.

Args:
flight_id: str

Returns:
bytes (KML XML)

Raises:
HTTP 404 Not Found: If the flight is not found
in the database.
"""
flight = await self.get_flight_by_id(flight_id)
flight_service = FlightService.from_flight_model(flight.flight)
return flight_service.get_flight_kml()
Comment on lines +166 to +168

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files:"
git ls-files | rg '(^|/)flight\.py$|api\.py$|rocketpy|kml|FlightService' || true

echo
echo "src/controllers/flight.py outline:"
ast-grep outline src/controllers/flight.py 2>/dev/null || true

echo
echo "Relevant src/controllers/flight.py lines:"
cat -n src/controllers/flight.py | sed -n '1,230p'

echo
echo "Search FlightService/get_flight_kml:"
rg -n "class FlightService|def get_flight_kml|from_flight_model|RocketPy|KML|rocket" src tests -S || true

Repository: RocketPy-Team/Infinity-API

Length of output: 50382


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Relevant src/routes/flight.py lines:"
cat -n src/routes/flight.py | sed -n '290,350p'

echo
echo "Relevant src/services/flight.py outline and contents:"
wc -l src/services/flight.py
ast-grep outline src/services/flight.py 2>/dev/null || true
cat -n src/services/flight.py | sed -n '1,340p'

echo
echo "Focused RocketPy KML usage in src/services/flight.py:"
rg -n -C 8 "kml|KML|Flight\(|save_kml|plot|export" src/services/flight.py src/controllers/flight.py || true

echo
echo "Check package versions/deps mentioning rocketpy:"
fd -a 'requirements|pyproject|poetry|Pipfile|environment|uv.lock' . | sed 's#^\./##' | sort
rg -n "rocketpy|rocketpy-dev|FastAPI|starlette|uvicorn|aiohttp|asyncio\.to_thread" -S --glob '!node_modules' --glob '!dist' --glob '!build' . | head -n 200

Repository: RocketPy-Team/Infinity-API

Length of output: 46105


Move KML generation off the event-loop thread.

get_flight_kml still runs FlightService.from_flight_model(...).get_flight_kml() synchronously after awaiting the database read, and the controller is awaited by the FastAPI route handler. The export creates a temp file, reads it, and deletes it synchronously, so a long KML export blocks the shared event loop.

Run the complete synchronous export path in a worker thread.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/controllers/flight.py` around lines 166 - 168, Update the controller
method around get_flight_by_id so the complete synchronous
FlightService.from_flight_model(...).get_flight_kml() export runs in a worker
thread after the awaited database read. Preserve the returned KML result while
ensuring temp-file creation, reading, and deletion do not execute on the
event-loop thread.


@controller_exception_handler
async def get_flight_simulation(
self,
Expand Down
6 changes: 5 additions & 1 deletion src/models/motor.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@ class MotorModel(ApiBaseModel):

# Required parameters
thrust_source: List[List[float]]
burn_time: float
# burn_time is optional for Liquid/Hybrid/Solid motors — rocketpy
# auto-detects the burn window from the thrust_source array span.
# GenericMotor still requires it; the motor service re-raises an
# explicit error when the GENERIC path receives None.
burn_time: Optional[float] = None
Comment on lines +23 to +27

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether MotorController.post_motor builds the rocketpy motor synchronously.
fd -e py motor.py --full-path src/controllers | xargs -I{} sh -c 'echo "--- {} ---"; cat -n {}'

Repository: RocketPy-Team/Infinity-API

Length of output: 3177


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Tracked controller files:"
git ls-files 'src/controllers/*.py'
echo "--- MotorController methods ---"
rg -n "def (create|create_?motor|post|get|update|delete|add|upsert|fetch)|motor_id|motor\.motor|MotorModel|get_motor_by_id" src/controllers/motor.py
echo "--- motor.py line count ---"
wc -l src/controllers/motor.py

Repository: RocketPy-Team/Infinity-API

Length of output: 1224


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- src/controllers/interface.py relevant sections ---"
wc -l src/controllers/interface.py
sed -n '1,220p' src/controllers/interface.py | cat -n

echo "--- src/api.py imports and registered motor/controller routes ---"
wc -l src/api.py
rg -n "MotorController|motor|controllers|router|BaseController|create" src/api.py -C 3

echo "--- MotorModel and service validation snippets ---"
wc -l src/models/motor.py src/services/motor.py
sed -n '1,130p' src/models/motor.py | cat -n
sed -n '220,260p' src/services/motor.py | cat -n

Repository: RocketPy-Team/Infinity-API

Length of output: 14542


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- create_motor methods in motor controllers/repositories ---"
rg -n "def create_motor|create_motor\\(" src/controllers src/repositories src/routes -C 6

echo "--- motor router outline and relevant snippets ---"
wc -l src/routes/motor.py
sed -n '1,220p' src/routes/motor.py | cat -n

echo "--- motor repository filenames and create_motor snippets ---"
git ls-files 'src/repositories/*motor*' 'src/repositories/motor.py'
fd -e py 'motor|Motor' src/repositories -x sh -c 'echo "--- $1 ---"; rg -n "create_motor|create_model|NAME" "$1" -C 5' sh {}

Repository: RocketPy-Team/Infinity-API

Length of output: 8123


Enforce GENERIC burn_time in the model validator.

POST /motors builds MotorModel, then MotorRepository.create_motor persists motor.model_dump(exclude_none=True). The GENERIC burn_time check lives only in MotorService.from_motor_model, so a malformed GENERIC motor can be stored and fail later. Add the same POST-level check used for solid/grain parameters and other motor-kind requirements in MotorModel.validate_motor_kind.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/models/motor.py` around lines 23 - 27, Update
MotorModel.validate_motor_kind to reject GENERIC motors when burn_time is None,
alongside the existing motor-kind requirements. Ensure this validation occurs
during POST model construction before MotorRepository.create_motor persists
model_dump(exclude_none=True), while preserving validation behavior for other
motor kinds.

nozzle_radius: float
dry_mass: float
dry_inertia: Tuple[float, float, float] = (0, 0, 0)
Expand Down
75 changes: 71 additions & 4 deletions src/models/sub/tanks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from enum import Enum
from typing import Optional, Tuple, List
from pydantic import BaseModel
from typing import Annotated, List, Literal, Optional, Tuple, Union

from pydantic import BaseModel, Field, model_validator


class TankKinds(str, Enum):
Expand All @@ -10,14 +11,64 @@ class TankKinds(str, Enum):
ULLAGE: str = "ULLAGE"


# Scalar: constant density (kg/m^3).
# [(temp_K, density_kg_m3)]: temperature-dependent (LOX / N2O).
# No pressure dependence.
DensityInput = Union[float, List[Tuple[float, float]]]


class TankFluids(BaseModel):
name: str
density: float
density: DensityInput


class CustomTankGeometry(BaseModel):
geometry_kind: Literal["custom"] = "custom"
geometry: List[Tuple[Tuple[float, float], float]]


class CylindricalTankGeometry(BaseModel):
geometry_kind: Literal["cylindrical"] = "cylindrical"
radius: float
height: float
spherical_caps: bool = False


class SphericalTankGeometry(BaseModel):
geometry_kind: Literal["spherical"] = "spherical"
radius: float


TankGeometryInput = Annotated[
Union[
CustomTankGeometry,
CylindricalTankGeometry,
SphericalTankGeometry,
],
Field(discriminator="geometry_kind"),
]


# Map tank_kind → tuple of MotorTank field names that rocketpy's
# corresponding Tank subclass requires.
_REQUIRED_FIELDS_BY_TANK_KIND = {
TankKinds.MASS_FLOW: (
"initial_liquid_mass",
"initial_gas_mass",
"liquid_mass_flow_rate_in",
"liquid_mass_flow_rate_out",
"gas_mass_flow_rate_in",
"gas_mass_flow_rate_out",
),
TankKinds.LEVEL: ("liquid_height",),
TankKinds.ULLAGE: ("ullage",),
TankKinds.MASS: ("liquid_mass", "gas_mass"),
}


class MotorTank(BaseModel):
# Required parameters
geometry: List[Tuple[Tuple[float, float], float]]
geometry: TankGeometryInput
gas: TankFluids
liquid: TankFluids
flux_time: Tuple[float, float]
Expand Down Expand Up @@ -48,3 +99,19 @@ class MotorTank(BaseModel):

# Computed parameters
tank_kind: TankKinds = TankKinds.MASS_FLOW

@model_validator(mode='after')
def validate_tank_kind_fields(self):
# reject incoherent payloads at the API boundary
# instead of letting rocketpy crash during Tank construction.
missing = [
field
for field in _REQUIRED_FIELDS_BY_TANK_KIND[self.tank_kind]
if getattr(self, field) is None
]
if missing:
raise ValueError(
f"tank_kind={self.tank_kind.value} requires: "
f"{', '.join(missing)}"
)
return self
36 changes: 36 additions & 0 deletions src/routes/flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,42 @@ async def update_flight_rocket(
)


@router.get(
"/{flight_id}/kml",
responses={
200: {
"description": "KML trajectory file download",
"content": {"application/vnd.google-earth.kml+xml": {}},
}
},
status_code=200,
response_class=Response,
)
async def get_flight_kml(
flight_id: str,
controller: FlightControllerDep,
):
"""
Export a flight trajectory as a KML file for Google Earth.

## Args
``` flight_id: str ```
"""
with tracer.start_as_current_span("get_flight_kml"):
kml = await controller.get_flight_kml(flight_id)
headers = {
"Content-Disposition": (
f'attachment; filename="flight_{flight_id}.kml"'
),
}
return Response(
content=kml,
headers=headers,
media_type="application/vnd.google-earth.kml+xml",
status_code=200,
)


@router.get("/{flight_id}/simulate")
async def get_flight_simulation(
flight_id: str,
Expand Down
25 changes: 25 additions & 0 deletions src/routes/motor.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,31 @@ async def create_motor(

## Args
``` models.Motor JSON ```

For liquid/hybrid motors the `tanks` field supports three geometry
kinds via the `geometry_kind` discriminator and a scalar-or-sampled
fluid `density`:

```
{
"motor_kind": "LIQUID",
...
"tanks": [{
"geometry": {
"geometry_kind": "cylindrical", // or "spherical", "custom"
"radius": 0.1, "height": 0.5
},
"liquid": {
"name": "LOX",
"density": [[90.0, 1141.0], [120.0, 1091.0]] // or scalar
},
"gas": {"name": "N2", "density": 1.2},
"tank_kind": "LEVEL",
"liquid_height": 0.25,
...
}]
}
```
"""
with tracer.start_as_current_span("create_motor"):
return await controller.post_motor(motor)
Expand Down
55 changes: 51 additions & 4 deletions src/services/flight.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import json
import os
import tempfile
from typing import Self, Tuple

import numpy as np

from rocketpy.simulation.flight import Flight as RocketPyFlight
from rocketpy.simulation.flight_data_exporter import FlightDataExporter
from rocketpy._encoders import RocketPyEncoder, RocketPyDecoder
from rocketpy.mathutils.function import Function
from rocketpy.motors.solid_motor import SolidMotor
Expand Down Expand Up @@ -224,6 +227,22 @@ def _to_float(value) -> float:
case _:
return float(value)

@staticmethod
def _extract_fluid_density(fluid):
"""Project a rocketpy Fluid's density back onto the API schema.

The API accepts either a scalar or a list of (T_K, density)
samples. Rocketpy may store density as either a raw scalar or a
``Function`` wrapping a 2D ``(T, P) -> density`` callable. A
full sample round-trip is not supported in this iteration;
Function-valued densities are collapsed to a scalar evaluated
at rocketpy's default reference (273.15 K, 101325 Pa).
"""
density = fluid.density
if isinstance(density, Function):
return float(density(273.15, 101325))
return density
Comment on lines +230 to +244

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Files matching flight.py:"
fd -a 'flight.py$' . || true

echo
echo "Outline src/services/flight.py:"
if [ -f src/services/flight.py ]; then
  wc -l src/services/flight.py
  ast-grep outline src/services/flight.py --view expanded | sed -n '1,220p'
fi

echo
echo "Relevant sections:"
for start in 1 180 210 280 380 460 540; do
  end=$((start+120))
  echo "--- src/services/flight.py $start-$end"
  sed -n "${start},${end}p" src/services/flight.py
done

echo
echo "Search DensityInput and extract_models:"
rg -n "DensityInput|extract_models|import_flight_from_rpy|_extract_fluid_density|density_input|fluid_dens|fluid" src tests | sed -n '1,240p'

Repository: RocketPy-Team/Infinity-API

Length of output: 33062


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "src/controllers/flight.py relevant section:"
sed -n '180,240p' src/controllers/flight.py | cat -n

echo
echo "src/services/motor.py fluid construction:"
sed -n '55,100p' src/services/motor.py | cat -n
sed -n '250,275p' src/services/motor.py | cat -n

echo
echo "MotorService round-trip usages:"
rg -n "from_models|from_motor_model|extract_models|get_models|_build_rocketpy_fluid|Fluid\\(" src/services/motor.py src/services/flight.py src/services/*.py | sed -n '1,220p'

echo
echo "Search for create flight from model round-trip:"
rg -n "FlightService\\.(from_flight_model|extract_models)|import_flight_from_rpy|from_motor_model" src tests | sed -n '1,220p'

Repository: RocketPy-Team/Infinity-API

Length of output: 10363


Preserve temperature-sampled fluid density during import.

_extract_fluid_density collapses function-backed tank gas and liquid density to one value at 273.15 K, then import_flight_from_rpy persists that scalar. A later simulation rebuilds fluids from those persisted samples and ignores the original temperature-dependent density. Extract representable (T_K, density) samples into DensityInput, or reject the import for non-representable density functions instead of changing the flight model.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/flight.py` around lines 230 - 244, Update _extract_fluid_density
and its import_flight_from_rpy caller to preserve temperature-dependent
densities by converting representable Function values into DensityInput (T_K,
density) samples; do not collapse them to a scalar at 273.15 K. If a density
function cannot be represented by the API’s sampled schema, reject the import
explicitly rather than persisting an inaccurate scalar.


@staticmethod
def _extract_tanks(motor) -> list[MotorTank]:
tanks: list[MotorTank] = []
Expand All @@ -240,20 +259,29 @@ def _extract_tanks(motor) -> list[MotorTank]:
case _:
tank_kind = TankKinds.MASS_FLOW

geometry = [
# Geometry round-trip is lossy: even if the client originally
# sent a cylindrical/spherical geometry, we discretise it back
# to the generic piecewise form on read. Every rocketpy tank
# geometry exposes its internal piecewise dict via
# `tank.geometry.geometry`, so this path covers all three
# geometry subclasses uniformly.
geometry_segments = [
(bounds, float(func(0)))
for bounds, func in tank.geometry.geometry.items()
]
Comment on lines +262 to 271

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant files =="
git ls-files | rg '(^src/services/flight.py$|rocketpy|tank|geometry)' | sed -n '1,120p'

echo
echo "== outline src/services/flight.py around import/export =="
ast-grep outline src/services/flight.py --view expanded 2>/dev/null | sed -n '1,220p' | rg -n 'def|class|flight|tank|import|serde|geometry' || true

echo
echo "== lines 220-310 =="
sed -n '220,310p' src/services/flight.py | nl -ba -v220

echo
echo "== search geometry classes/config =="
rg -n "class .*Tank|Geometry|geometry\.geometry|TankGeometry|Tank|tank\.geometry|rocketpy" src tests -g '*.py' | sed -n '1,240p'

Repository: RocketPy-Team/Infinity-API

Length of output: 1357


🌐 Web query:

rocketpy geometry TankGeometry geometry attribute cylindrical spherical piecewise

💡 Result:

In RocketPy, the TankGeometry class is used to define the geometry of an axi-symmetric tank by specifying its radius as a function of its height along the axis of symmetry [1][2]. The geometry is defined using a dictionary provided to the geometry_dict parameter [3][4]. This dictionary allows for the construction of complex, piecewise defined shapes [1][5]. Key details regarding the TankGeometry attribute: 1. The geometry attribute is a dictionary where each key is a disjoint tuple (lower_bound, upper_bound) representing a domain along the tank's symmetry axis in meters [1][3]. 2. The corresponding values are functions (typically Python lambdas or RocketPy.Function objects) that map the tank's height to its radius at that specific section [1][2]. 3. Because the dictionary can contain multiple disjoint domains, the tank's overall radius profile is effectively a piecewise-defined function [1][5][6]. While users can manually define geometries using this dictionary approach for arbitrary shapes, RocketPy also provides convenience classes such as CylindricalTank and SphericalTank for standard configurations [3][4]. For example, a spherical tank can be defined using the geometry dictionary by specifying a radius function such as lambda h: (radius2 - h2)(1/2) over the domain (-radius, radius) [1][2]. The internal logic of TankGeometry then computes the overall radius, area, and volume functions based on these inputs [1][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== src/services/flight.py lines 247-330 =="
sed -n '247,330p' src/services/flight.py

echo
echo "== src/models/sub/tanks.py =="
cat -n src/models/sub/tanks.py

echo
echo "== imports and serialization helpers =="
sed -n '1,120p' src/services/flight.py

echo
echo "== tank field serialization references =="
rg -n "MotorTank|tank|geometry|_extract_tanks|rpy|from_rpy" src -g '*.py' | sed -n '1,260p'

Repository: RocketPy-Team/Infinity-API

Length of output: 28255


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
import ast
from pathlib import Path

src = Path('src/services/flight.py').read_text()
tree = ast.parse(src, filename='src/services/flight.py')
for node in tree.body:
    if node.name == 'FlightService':
        for cls in node.body:
            if isinstance(cls, ast.FunctionDef) and cls.name == '_extract_tanks':
                src_snippet = src.splitlines()[cls.lineno-1:cls.end_lineno]
                print(f'function: {cls.name} lines {cls.lineno}-{cls.end_lineno}')
                lines = [
                    ('geometry_segments =', any('geometry_segments = [' in line for line in src_snippet)),
                    ('evaluating func(0) explicitly:', any('func(0)' in line for line in src_snippet)),
                    ('custom geometry_kind:', any('geometry_kind": "custom"' in line for line in src_snippet)),
                    ('tank geometry dict:', any('tank.geometry.geometry.items()' in line for line in src_snippet)),
                ]
                for label, ok in lines:
                    print(f'{label}: {ok!s}')
                print('target block:')
                for i, line in enumerate(src_snippet, start=cls.lineno):
                    print(f'{i}: {line}')
PY

Repository: RocketPy-Team/Infinity-API

Length of output: 442


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== inspect _build_rocketpy_tank_geometry =="
sed -n '40,62p' src/services/motor.py

echo
echo "== inspect _generate_positioned_tanks if present in installed rocketpy =="
python3 - <<'PY'
import inspect
try:
    import rocketpy
    from rocketpy import LevelBasedTank, MassBasedTank, UllageBasedTank
    print('rocketpy imported', getattr(rocketpy, '__version__', ''))
    for name, cls in [('LevelBasedTank', LevelBasedTank), ('MassBasedTank', MassBasedTank), ('UllageBasedTank', UllageBasedTank)]:
        print(f'\n{name}:')
        print(inspect.signature(cls))
        print('methods:', sorted([m for m in dir(cls) if not m.startswith('_') and callable(getattr(cls, m))]))
        if hasattr(cls, 'plots') and hasattr(getattr(cls, 'plots'), '_generate_positioned_tanks'):
            print(inspect.getsource(cls.plots._generate_positioned_tanks))
except Exception as exc:
    print(type(exc).__name__ + ': ' + str(exc))
PY

Repository: RocketPy-Team/Infinity-API

Length of output: 1101


Preserve position-dependent tank geometry during import.

_extract_tanks() collapses each shape into a geometry_kind: "custom" segment whose radius is sampled at position == 0. For spherical tanks, the radius depends on tank height, so the imported geometry changes volume and later .rpy-loaded motor simulations. Preserve the discriminated cylindrical/spherical forms, or serialize the full variable-radius custom geometry instead of collapsing it to a constant-radius segment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/services/flight.py` around lines 262 - 271, Update _extract_tanks() and
the geometry_segments construction to preserve position-dependent tank geometry
during import: do not sample each function only at position 0 or collapse
cylindrical/spherical shapes into constant-radius custom segments. Retain the
discriminated cylindrical/spherical representation, or serialize the complete
variable-radius custom geometry so spherical tank radii remain height-dependent
and downstream .rpy motor simulations preserve volume.


data: dict = {
"geometry": geometry,
"geometry": {
"geometry_kind": "custom",
"geometry": geometry_segments,
},
"gas": TankFluids(
name=tank.gas.name,
density=tank.gas.density,
density=FlightService._extract_fluid_density(tank.gas),
),
"liquid": TankFluids(
name=tank.liquid.name,
density=tank.liquid.density,
density=FlightService._extract_fluid_density(tank.liquid),
),
"flux_time": tank.flux_time,
"position": position,
Expand Down Expand Up @@ -474,6 +502,25 @@ def get_flight_simulation(self) -> FlightSimulation:
flight_simulation = FlightSimulation(**encoded_attributes)
return flight_simulation

def get_flight_kml(self) -> bytes:
"""
Get the flight trajectory as a KML file for Google Earth.

Returns:
bytes (UTF-8 encoded KML)
"""
with tempfile.NamedTemporaryFile(
suffix=".kml", delete=False
) as tmp:
tmp_path = tmp.name
try:
FlightDataExporter(self.flight).export_kml(file_name=tmp_path)
with open(tmp_path, "rb") as fh:
return fh.read()
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)

def get_flight_rpy(self) -> bytes:
"""
Get the portable JSON ``.rpy`` representation of the flight.
Expand Down
Loading
Loading