diff --git a/Makefile b/Makefile index c65bc7f..337c8b2 100644 --- a/Makefile +++ b/Makefile @@ -1,27 +1,69 @@ +VENV_BIN ?= ./infinity_env/bin +PYLINTHOME ?= .pylint.d + +export PYLINTHOME + +ifneq (,$(wildcard $(VENV_BIN)/black)) +BLACK := $(VENV_BIN)/black +else +BLACK := black +endif + +ifneq (,$(wildcard $(VENV_BIN)/ruff)) +RUFF := $(VENV_BIN)/ruff +else +RUFF := ruff +endif + +ifneq (,$(wildcard $(VENV_BIN)/flake8)) +FLAKE8 := $(VENV_BIN)/flake8 +else +FLAKE8 := flake8 +endif + +ifneq (,$(wildcard $(VENV_BIN)/pylint)) +PYLINT := $(VENV_BIN)/pylint +else +PYLINT := pylint +endif + +ifneq (,$(wildcard $(VENV_BIN)/pytest)) +PYTEST := $(VENV_BIN)/pytest +else +PYTEST := python3 -m pytest +endif + +ifneq (,$(wildcard $(VENV_BIN)/uvicorn)) +UVICORN := $(VENV_BIN)/uvicorn +else +UVICORN := python3 -m uvicorn +endif + format: black ruff lint: flake8 pylint black: - black ./src || true - black ./tests || true + $(BLACK) ./src || true + $(BLACK) ./tests || true flake8: - flake8 --ignore E501,E402,F401,W503,C0414 ./src || true - flake8 --ignore E501,E402,F401,W503,C0414 ./tests || true + $(FLAKE8) --ignore E501,E402,F401,W503,C0414 ./src || true + $(FLAKE8) --ignore E501,E402,F401,W503,C0414 ./tests || true pylint: - pylint ./src || true - pylint ./tests || true + @mkdir -p $(PYLINTHOME) + $(PYLINT) ./src || true + $(PYLINT) ./tests || true ruff: - ruff check --fix ./src || true - ruff check --fix ./tests || true + $(RUFF) check --fix ./src || true + $(RUFF) check --fix ./tests || true test: - python3 -m pytest . + $(PYTEST) . dev: - python3 -m uvicorn src:app --reload --port 3000 --loop uvloop + $(UVICORN) src:app --reload --port 3000 --loop uvloop clean: docker stop infinity-api diff --git a/src/models/sub/aerosurfaces.py b/src/models/sub/aerosurfaces.py index 339066e..b6dd590 100644 --- a/src/models/sub/aerosurfaces.py +++ b/src/models/sub/aerosurfaces.py @@ -44,14 +44,18 @@ class Fins(BaseModel): sweep_length: Optional[float] = None sweep_angle: Optional[float] = None + _base_keys = {"fins_kind", "name", "n", "root_chord", "span", "position"} + def get_additional_parameters(self): - return { - key: value - for key, value in self.dict().items() - if value is not None - and key - not in ["fins_kind", "name", "n", "root_chord", "span", "position"] + params = { + k: v + for k, v in self.dict().items() + if v is not None and k not in self._base_keys } + if "sweep_angle" in params and "sweep_length" in params: + params.pop("sweep_length") + + return params # TODO: implement airbrakes diff --git a/src/utils.py b/src/utils.py index d24d724..c964e3f 100644 --- a/src/utils.py +++ b/src/utils.py @@ -54,10 +54,8 @@ def for_flight(cls) -> 'DiscretizeConfig': class InfinityEncoder(RocketPyEncoder): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def default(self, obj): + def default(self, o): + obj = o if ( isinstance(obj, Function) and not callable(obj.source) @@ -75,7 +73,7 @@ def default(self, obj): mutate_self=False, ) if isinstance(obj, Flight): - obj._Flight__evaluate_post_process + obj._Flight__evaluate_post_process() solution = np.array(obj.solution) size = len(solution) if size > 25: @@ -117,92 +115,80 @@ def rocketpy_encoder(obj): def collect_attributes(obj, attribute_classes=None): - """ - Collect attributes from various simulation classes and populate them from the flight object. - """ - if attribute_classes is None: - attribute_classes = [] + """Collect and serialize attributes from simulation classes.""" + attribute_classes = attribute_classes or [] attributes = rocketpy_encoder(obj) - for attribute_class in attribute_classes: - if issubclass(attribute_class, FlightSimulation): - flight_attributes_list = [ - attr - for attr in attribute_class.__annotations__.keys() - if attr not in ["message", "rocket", "env"] - ] - try: - for key in flight_attributes_list: - if key not in attributes: - try: - value = getattr(obj, key) - attributes[key] = value - except Exception: - pass - except Exception: - pass - - elif issubclass(attribute_class, RocketSimulation): - rocket_attributes_list = [ - attr - for attr in attribute_class.__annotations__.keys() - if attr not in ["message", "motor"] - ] - try: - for key in rocket_attributes_list: - if key not in attributes.get("rocket", {}): - try: - value = getattr(obj.rocket, key) - attributes.setdefault("rocket", {})[key] = value - except Exception: - pass - except Exception: - pass - - elif issubclass(attribute_class, MotorSimulation): - motor_attributes_list = [ - attr - for attr in attribute_class.__annotations__.keys() - if attr not in ["message"] - ] - try: - for key in motor_attributes_list: - if key not in attributes.get("rocket", {}).get( - "motor", {} - ): - try: - value = getattr(obj.rocket.motor, key) - attributes.setdefault("rocket", {}).setdefault( - "motor", {} - )[key] = value - except Exception: - pass - except Exception: - pass - - elif issubclass(attribute_class, EnvironmentSimulation): - environment_attributes_list = [ - attr - for attr in attribute_class.__annotations__.keys() - if attr not in ["message"] - ] - try: - for key in environment_attributes_list: - if key not in attributes.get("env", {}): - try: - value = getattr(obj.env, key) - attributes.setdefault("env", {})[key] = value - except Exception: - pass - except Exception: - pass - else: - continue + _populate_simulation_attributes(obj, attribute_class, attributes) return rocketpy_encoder(attributes) +def _populate_simulation_attributes(obj, attribute_class, attributes): + if not isinstance(attribute_class, type): + return + + mappings = ( + (FlightSimulation, (), (), {"message", "rocket", "env"}), + (RocketSimulation, ("rocket",), ("rocket",), {"message", "motor"}), + ( + MotorSimulation, + ("rocket", "motor"), + ("rocket", "motor"), + {"message"}, + ), + (EnvironmentSimulation, ("env",), ("env",), {"message"}), + ) + + for klass, source_path, target_path, exclusions in mappings: + if not issubclass(attribute_class, klass): + continue + + keys = _annotation_keys(attribute_class, exclusions) + if not keys: + return + + source = _resolve_attribute_path(obj, source_path) + if source is None: + return + + target = _resolve_attribute_target(attributes, target_path) + _copy_missing_attributes(source, target, keys) + return + + +def _annotation_keys(attribute_class, exclusions): + annotations = getattr(attribute_class, "__annotations__", {}) + return [key for key in annotations if key not in exclusions] + + +def _resolve_attribute_path(root, path): + current = root + for attr in path: + if current is None: + return None + current = getattr(current, attr, None) + return current + + +def _resolve_attribute_target(attributes, path): + target = attributes + for key in path: + target = target.setdefault(key, {}) + return target + + +def _copy_missing_attributes(source, target, keys): + for key in keys: + if key in target: + continue + try: + target[key] = getattr(source, key) + except AttributeError: + continue + + def _fix_datetime_fields(data): """ Fix datetime fields that RocketPyEncoder converted to lists.