|
| 1 | +import base64 |
1 | 2 | import re |
| 3 | +import tempfile |
2 | 4 | import warnings |
3 | 5 | import xml.etree.ElementTree as ET |
4 | 6 | from abc import ABC, abstractmethod |
5 | 7 | from functools import cached_property |
6 | | -from os import path |
| 8 | +from os import path, remove |
7 | 9 |
|
8 | 10 | import numpy as np |
| 11 | +import requests |
9 | 12 |
|
10 | 13 | from ..mathutils.function import Function, funcify_method |
11 | 14 | from ..plots.motor_plots import _MotorPlots |
@@ -1914,6 +1917,121 @@ def load_from_rse_file( |
1914 | 1917 | coordinate_system_orientation=coordinate_system_orientation, |
1915 | 1918 | ) |
1916 | 1919 |
|
| 1920 | + @staticmethod |
| 1921 | + def _call_thrustcurve_api(name: str): |
| 1922 | + """ |
| 1923 | + Download a .eng file from the ThrustCurve API |
| 1924 | + based on the given motor name. |
| 1925 | +
|
| 1926 | + Parameters |
| 1927 | + ---------- |
| 1928 | + name : str |
| 1929 | + The motor name according to the API (e.g., "Cesaroni_M1670" or "M1670"). |
| 1930 | + Both manufacturer-prefixed and shorthand names are commonly used; if multiple |
| 1931 | + motors match the search, the first result is used. |
| 1932 | +
|
| 1933 | + Returns |
| 1934 | + ------- |
| 1935 | + data_base64 : str |
| 1936 | + The .eng file of the motor in base64 |
| 1937 | +
|
| 1938 | + Raises |
| 1939 | + ------ |
| 1940 | + ValueError |
| 1941 | + If no motor is found or if the downloaded .eng data is missing. |
| 1942 | + requests.exceptions.RequestException |
| 1943 | + If a network or HTTP error occurs during the API call. |
| 1944 | + """ |
| 1945 | + base_url = "https://www.thrustcurve.org/api/v1" |
| 1946 | + |
| 1947 | + # Step 1. Search motor |
| 1948 | + response = requests.get(f"{base_url}/search.json", params={"commonName": name}) |
| 1949 | + response.raise_for_status() |
| 1950 | + data = response.json() |
| 1951 | + |
| 1952 | + if not data.get("results"): |
| 1953 | + raise ValueError( |
| 1954 | + f"No motor found for name '{name}'. " |
| 1955 | + "Please verify the motor name format (e.g., 'Cesaroni_M1670' or 'M1670') and try again." |
| 1956 | + ) |
| 1957 | + |
| 1958 | + motor_info = data["results"][0] |
| 1959 | + motor_id = motor_info.get("motorId") |
| 1960 | + # NOTE: commented bc we don't use it, but keeping for possible future use |
| 1961 | + # designation = motor_info.get("designation", "").replace("/", "-") |
| 1962 | + # manufacturer = motor_info.get("manufacturer", "") |
| 1963 | + |
| 1964 | + # Step 2. Download the .eng file |
| 1965 | + dl_response = requests.get( |
| 1966 | + f"{base_url}/download.json", |
| 1967 | + params={"motorIds": motor_id, "format": "RASP", "data": "file"}, |
| 1968 | + ) |
| 1969 | + dl_response.raise_for_status() |
| 1970 | + dl_data = dl_response.json() |
| 1971 | + |
| 1972 | + if not dl_data.get("results"): |
| 1973 | + raise ValueError( |
| 1974 | + f"No .eng file found for motor '{name}' in the ThrustCurve API." |
| 1975 | + ) |
| 1976 | + |
| 1977 | + data_base64 = dl_data["results"][0].get("data") |
| 1978 | + if not data_base64: |
| 1979 | + raise ValueError( |
| 1980 | + f"Downloaded .eng data for motor '{name}' is empty or invalid." |
| 1981 | + ) |
| 1982 | + return data_base64 |
| 1983 | + |
| 1984 | + @staticmethod |
| 1985 | + def load_from_thrustcurve_api(name: str, **kwargs): |
| 1986 | + """ |
| 1987 | + Creates a Motor instance by downloading a .eng file from the ThrustCurve API |
| 1988 | + based on the given motor name. |
| 1989 | +
|
| 1990 | + Parameters |
| 1991 | + ---------- |
| 1992 | + name : str |
| 1993 | + The motor name according to the API (e.g., "Cesaroni_M1670" or "M1670"). |
| 1994 | + Both manufacturer-prefixed and shorthand names are commonly used; if multiple |
| 1995 | + motors match the search, the first result is used. |
| 1996 | + **kwargs : |
| 1997 | + Additional arguments passed to the Motor constructor or loader, such as |
| 1998 | + dry_mass, nozzle_radius, etc. |
| 1999 | +
|
| 2000 | + Returns |
| 2001 | + ------- |
| 2002 | + instance : GenericMotor |
| 2003 | + A new GenericMotor instance initialized using the downloaded .eng file. |
| 2004 | +
|
| 2005 | + Raises |
| 2006 | + ------ |
| 2007 | + ValueError |
| 2008 | + If no motor is found or if the downloaded .eng data is missing. |
| 2009 | + requests.exceptions.RequestException |
| 2010 | + If a network or HTTP error occurs during the API call. |
| 2011 | + """ |
| 2012 | + |
| 2013 | + data_base64 = GenericMotor._call_thrustcurve_api(name) |
| 2014 | + data_bytes = base64.b64decode(data_base64) |
| 2015 | + |
| 2016 | + # Step 3. Create the motor from the .eng file |
| 2017 | + tmp_path = None |
| 2018 | + try: |
| 2019 | + # create a temporary file that persists until we explicitly remove it |
| 2020 | + with tempfile.NamedTemporaryFile(suffix=".eng", delete=False) as tmp_file: |
| 2021 | + tmp_file.write(data_bytes) |
| 2022 | + tmp_file.flush() |
| 2023 | + tmp_path = tmp_file.name |
| 2024 | + |
| 2025 | + return GenericMotor.load_from_eng_file(tmp_path, **kwargs) |
| 2026 | + finally: |
| 2027 | + # Ensuring the temporary file is removed |
| 2028 | + if tmp_path and path.exists(tmp_path): |
| 2029 | + try: |
| 2030 | + remove(tmp_path) |
| 2031 | + except OSError: |
| 2032 | + # If cleanup fails, don't raise: we don't want to mask prior exceptions. |
| 2033 | + pass |
| 2034 | + |
1917 | 2035 | def all_info(self): |
1918 | 2036 | """Prints out all data and graphs available about the Motor.""" |
1919 | 2037 | # Print motor details |
|
0 commit comments