Skip to content

Commit

Permalink
fixes motor and rocket summary route, improves separation of concerns
Browse files Browse the repository at this point in the history
fixes summary routes

migrates from jsonpickle to dill binary

pin pydantic version to ensure compatibility; upgrades python base image

implements generic motor; removes rocket_options; fixes binary output

addresses PR review

increases response timeout; minor refactor on importing statements; fix parachute trigger evaluation context

fixes pylint issues

Updates pylint python version
  • Loading branch information
GabrielBarberini committed Sep 11, 2024
1 parent 8aeaf82 commit 57d0a69
Show file tree
Hide file tree
Showing 44 changed files with 1,159 additions and 1,229 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.11.5"]
python-version: ["3.12.5"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.11-slim-bookworm
FROM python:3.12.5-slim-bookworm

EXPOSE 3000

Expand All @@ -16,4 +16,4 @@ RUN apt-get update && \

COPY ./lib /app/lib

CMD ["gunicorn", "-c", "lib/settings/gunicorn.py", "-w", "1", "--threads=2", "-k", "uvicorn.workers.UvicornWorker", "lib.api:app", "--log-level", "Debug", "-b", "0.0.0.0:3000", "--timeout", "35"]
CMD ["gunicorn", "-c", "lib/settings/gunicorn.py", "-w", "1", "--threads=2", "-k", "uvicorn.workers.UvicornWorker", "lib.api:app", "--log-level", "Debug", "-b", "0.0.0.0:3000", "--timeout", "60"]
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@
- [install mongodb-atlas](https://www.mongodb.com/try/download/community)
- Install dependencies `python3 -m pip install -r requirements.txt`

## Development
- black ./lib
- pylint --extension-pkg-whitelist='pydantic' ./lib/*
- flake8 --ignore E501,E402,F401,W503 ./lib

## Starting the server
- Setup MONGODB_CONNECTION_STRING:
```
Expand Down Expand Up @@ -62,7 +67,6 @@ $ touch .env && echo MONGODB_CONNECTION_STRING="$ConnectionString" > .env
│   │   ├── environment.py
│   │   ├── flight.py
│   │   ├── motor.py
│   │   ├── parachute.py
│   │   └── rocket.py
│   │  
│   └── views
Expand Down Expand Up @@ -163,7 +167,7 @@ sequenceDiagram
participant MongoDB
participant Rocketpy lib
User ->> API: POST /simulate/rocketpy-model/:id
User ->> API: POST /summary/rocketpy-model/:id
API -->> MongoDB: Retrieve Rocketpy Model
MongoDB -->> API: Rocketpy Model
API ->> Rocketpy lib: Simulate Rocketpy Model
Expand Down
2 changes: 1 addition & 1 deletion lib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@ def parse_error(error):
return f"{exc_type}: {exc_obj}"


from lib.api import app
from lib.api import app # pylint: disable=wrong-import-position,cyclic-import
2 changes: 1 addition & 1 deletion lib/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
from lib.api import app

if __name__ == '__main__':
app.run()
app.run() # pylint: disable=no-member
8 changes: 4 additions & 4 deletions lib/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
from fastapi import FastAPI, Request, status
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.openapi.utils import get_openapi
from fastapi.responses import RedirectResponse, JSONResponse

Expand All @@ -14,13 +13,15 @@

from lib import logger, parse_error
from lib.routes import flight, environment, motor, rocket
from lib.utils import RocketPyGZipMiddleware

app = FastAPI(
swagger_ui_parameters={
"defaultModelsExpandDepth": 0,
"syntaxHighlight.theme": "obsidian",
}
)

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
Expand All @@ -37,15 +38,15 @@
RequestsInstrumentor().instrument()

# Compress responses above 1KB
app.add_middleware(GZipMiddleware, minimum_size=1000)
app.add_middleware(RocketPyGZipMiddleware, minimum_size=1000)


def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title="RocketPy Infinity-API",
version="1.2.2 BETA",
version="2.0.0",
description=(
"<p style='font-size: 18px;'>RocketPy Infinity-API is a RESTful Open API for RocketPy, a rocket flight simulator.</p>"
"<br/>"
Expand All @@ -57,7 +58,6 @@ def custom_openapi():
"<a href='https://api.rocketpy.org/redoc' style='color: white; text-decoration: none;'>ReDoc</a>"
"</button>"
"<p>Create, manage, and simulate rocket flights, environments, rockets, and motors.</p>"
"<p>Currently, the API only supports TrapezoidalFins. We apologize for the limitation, but we are actively working to expand its capabilities soon.</p>"
"<p>Please report any bugs at <a href='https://github.com/RocketPy-Team/infinity-api/issues/new/choose' style='text-decoration: none; color: #008CBA;'>GitHub Issues</a></p>"
),
routes=app.routes,
Expand Down
34 changes: 15 additions & 19 deletions lib/controllers/environment.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from typing import Union

import jsonpickle
from fastapi import HTTPException, status
from pymongo.errors import PyMongoError

Expand All @@ -13,7 +12,6 @@
EnvCreated,
EnvDeleted,
EnvUpdated,
EnvPickle,
)


Expand Down Expand Up @@ -91,7 +89,7 @@ async def get_env_by_id(env_id: str) -> Union[Env, HTTPException]:
try:
async with EnvRepository() as env_repo:
await env_repo.get_env_by_id(env_id)
read_env = env_repo.env
env = env_repo.env
except PyMongoError as e:
logger.error(
f"controllers.environment.get_env_by_id: PyMongoError {e}"
Expand All @@ -110,8 +108,8 @@ async def get_env_by_id(env_id: str) -> Union[Env, HTTPException]:
detail=f"Failed to read environment: {exc_str}",
) from e
else:
if read_env:
return read_env
if env:
return env
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Environment not found",
Expand All @@ -122,43 +120,41 @@ async def get_env_by_id(env_id: str) -> Union[Env, HTTPException]:
)

@classmethod
async def get_rocketpy_env_as_jsonpickle(
async def get_rocketpy_env_binary(
cls,
env_id: str,
) -> Union[EnvPickle, HTTPException]:
) -> Union[bytes, HTTPException]:
"""
Get rocketpy.Environmnet as jsonpickle string.
Get rocketpy.Environmnet dill binary.
Args:
env_id: str
Returns:
views.EnvPickle
bytes
Raises:
HTTP 404 Not Found: If the env is not found in the database.
"""
try:
read_env = await cls.get_env_by_id(env_id)
rocketpy_env = EnvironmentService.from_env_model(read_env)
env = await cls.get_env_by_id(env_id)
env_service = EnvironmentService.from_env_model(env)
except HTTPException as e:
raise e from e
except Exception as e:
exc_str = parse_error(e)
logger.error(
f"controllers.environment.get_rocketpy_env_as_jsonpickle: {exc_str}"
f"controllers.environment.get_rocketpy_env_as_binary: {exc_str}"
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to read environment: {exc_str}",
) from e
else:
return EnvPickle(
jsonpickle_rocketpy_env=jsonpickle.encode(rocketpy_env)
)
return env_service.get_env_binary()
finally:
logger.info(
f"Call to controllers.environment.get_rocketpy_env_as_jsonpickle completed for Env {env_id}"
f"Call to controllers.environment.get_rocketpy_env_binary completed for Env {env_id}"
)

async def update_env_by_id(
Expand Down Expand Up @@ -263,9 +259,9 @@ async def simulate_env(
HTTP 404 Not Found: If the env does not exist in the database.
"""
try:
read_env = await cls.get_env_by_id(env_id)
rocketpy_env = EnvironmentService.from_env_model(read_env)
env_summary = rocketpy_env.get_env_summary()
env = await cls.get_env_by_id(env_id)
env_service = EnvironmentService.from_env_model(env)
env_summary = env_service.get_env_summary()
except HTTPException as e:
raise e from e
except Exception as e:
Expand Down
81 changes: 45 additions & 36 deletions lib/controllers/flight.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@
from pymongo.errors import PyMongoError


import jsonpickle

from lib import logger, parse_error
from lib.controllers.rocket import RocketController
from lib.models.environment import Env
from lib.models.rocket import Rocket
from lib.models.flight import Flight
from lib.views.motor import MotorView
from lib.views.rocket import RocketView
from lib.views.flight import (
FlightSummary,
FlightCreated,
FlightUpdated,
FlightDeleted,
FlightPickle,
FlightView,
)
from lib.repositories.flight import FlightRepository
from lib.services.flight import FlightService
Expand Down Expand Up @@ -42,6 +43,7 @@ def __init__(
self,
flight: Flight,
):
self.guard(flight)
self._flight = flight

@property
Expand All @@ -52,6 +54,10 @@ def flight(self) -> Flight:
def flight(self, flight: Flight):
self._flight = flight

@staticmethod
def guard(flight: Flight):
RocketController.guard(flight.rocket)

async def create_flight(self) -> Union[FlightCreated, HTTPException]:
"""
Create a flight in the database.
Expand Down Expand Up @@ -85,7 +91,9 @@ async def create_flight(self) -> Union[FlightCreated, HTTPException]:
)

@staticmethod
async def get_flight_by_id(flight_id: str) -> Union[Flight, HTTPException]:
async def get_flight_by_id(
flight_id: str,
) -> Union[FlightView, HTTPException]:
"""
Get a flight from the database.
Expand All @@ -101,7 +109,7 @@ async def get_flight_by_id(flight_id: str) -> Union[Flight, HTTPException]:
try:
async with FlightRepository() as flight_repo:
await flight_repo.get_flight_by_id(flight_id)
read_flight = flight_repo.flight
flight = flight_repo.flight
except PyMongoError as e:
logger.error(
f"controllers.flight.get_flight_by_id: PyMongoError {e}"
Expand All @@ -120,8 +128,20 @@ async def get_flight_by_id(flight_id: str) -> Union[Flight, HTTPException]:
detail=f"Failed to read flight: {exc_str}",
) from e
else:
if read_flight:
return read_flight
if flight:
motor_view = MotorView(
**flight.rocket.motor.dict(),
selected_motor_kind=flight.rocket.motor.motor_kind,
)
updated_rocket = flight.rocket.dict()
updated_rocket.update(motor=motor_view)
rocket_view = RocketView(
**updated_rocket,
)
updated_flight = flight.dict()
updated_flight.update(rocket=rocket_view)
flight_view = FlightView(**updated_flight)
return flight_view
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Flight not found.",
Expand All @@ -132,43 +152,41 @@ async def get_flight_by_id(flight_id: str) -> Union[Flight, HTTPException]:
)

@classmethod
async def get_rocketpy_flight_as_jsonpickle(
async def get_rocketpy_flight_binary(
cls,
flight_id: str,
) -> Union[FlightPickle, HTTPException]:
) -> Union[bytes, HTTPException]:
"""
Get rocketpy.flight as jsonpickle string.
Get rocketpy.flight as dill binary.
Args:
flight_id: str
Returns:
views.FlightPickle
bytes
Raises:
HTTP 404 Not Found: If the flight is not found in the database.
"""
try:
read_flight = await cls.get_flight_by_id(flight_id)
rocketpy_flight = FlightService.from_flight_model(read_flight)
flight = await cls.get_flight_by_id(flight_id)
flight_service = FlightService.from_flight_model(flight)
except HTTPException as e:
raise e from e
except Exception as e:
exc_str = parse_error(e)
logger.error(
f"controllers.flight.get_rocketpy_flight_as_jsonpickle: {exc_str}"
f"controllers.flight.get_rocketpy_flight_binary: {exc_str}"
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Failed to read flight: {exc_str}",
) from e
else:
return FlightPickle(
jsonpickle_rocketpy_flight=jsonpickle.encode(rocketpy_flight)
)
return flight_service.get_flight_binary()
finally:
logger.info(
f"Call to controllers.flight.get_rocketpy_flight_as_jsonpickle completed for Flight {flight_id}"
f"Call to controllers.flight.get_rocketpy_flight_binary completed for Flight {flight_id}"
)

async def update_flight_by_id(
Expand Down Expand Up @@ -229,11 +247,9 @@ async def update_env_by_flight_id(
HTTP 404 Not Found: If the flight is not found in the database.
"""
try:
read_flight = await cls.get_flight_by_id(flight_id)
new_flight = read_flight.dict()
new_flight["environment"] = env
new_flight = Flight(**new_flight)
async with FlightRepository(new_flight) as flight_repo:
flight = await cls.get_flight_by_id(flight_id)
flight.environment = env
async with FlightRepository(flight) as flight_repo:
await flight_repo.update_env_by_flight_id(flight_id)
except PyMongoError as e:
logger.error(
Expand Down Expand Up @@ -277,16 +293,9 @@ async def update_rocket_by_flight_id(
HTTP 404 Not Found: If the flight is not found in the database.
"""
try:
read_flight = await cls.get_flight_by_id(flight_id)
updated_rocket = rocket.dict()
updated_rocket["rocket_option"] = rocket.rocket_option.value
updated_rocket["motor"][
"motor_kind"
] = rocket.motor.motor_kind.value
new_flight = read_flight.dict()
new_flight["rocket"] = updated_rocket
new_flight = Flight(**new_flight)
async with FlightRepository(new_flight) as flight_repo:
flight = await cls.get_flight_by_id(flight_id)
flight.rocket = rocket
async with FlightRepository(flight) as flight_repo:
await flight_repo.update_rocket_by_flight_id(flight_id)
except PyMongoError as e:
logger.error(
Expand Down Expand Up @@ -371,9 +380,9 @@ async def simulate_flight(
HTTP 404 Not Found: If the flight does not exist in the database.
"""
try:
read_flight = await cls.get_flight_by_id(flight_id=flight_id)
rocketpy_flight = FlightService.from_flight_model(read_flight)
flight_summary = rocketpy_flight.get_flight_summary()
flight = await cls.get_flight_by_id(flight_id=flight_id)
flight_service = FlightService.from_flight_model(flight)
flight_summary = flight_service.get_flight_summary()
except HTTPException as e:
raise e from e
except Exception as e:
Expand Down
Loading

0 comments on commit 57d0a69

Please sign in to comment.