def __init__( self, api_url: str, start_location: Union[Tuple[float, float, Optional[float]], Point], start_time: datetime, ascent_rate: float, burst_altitude: float, sea_level_descent_rate: float, float_altitude: float = None, float_duration: timedelta = None, name: str = None, descent_only: bool = False, ): """ :param api_url: URL of API :param start_location: location of balloon launch :param start_time: date and time of balloon launch :param ascent_rate: average ascent rate (m/s) :param burst_altitude: altitude at which balloon will burst :param sea_level_descent_rate: descent rate at sea level (m/s) :param float_altitude: altitude of float (m) :param float_duration: date and time of float end :param name: name of prediction track :param descent_only: whether to query for descent only """ if not isinstance(start_location, Point): start_location = typepigeon.convert_value(start_location, Point) if name is None: name = "prediction" if start_time is not None: if not isinstance(start_time, datetime): start_time = typepigeon.convert_value(start_time, datetime) if (start_time.tzinfo is None or start_time.tzinfo.utcoffset(start_time) is None): start_time = UTC_TIMEZONE.localize(start_time) if float_duration is not None: if not isinstance(float_duration, datetime): float_duration = typepigeon.convert_value( float_duration, timedelta) self.api_url = api_url self.start_location = start_location self.start_time = start_time self.ascent_rate = ascent_rate self.burst_altitude = burst_altitude self.sea_level_descent_rate = abs(sea_level_descent_rate) self.float_altitude = float_altitude self.float_duration = float_duration self.name = name self.descent_only = descent_only
def __setitem__(self, key: str, value: Any): if key in self.fields: field_type = self.fields[key] if not isinstance(field_type, type): field_type = type(field_type) if not isinstance(value, field_type): value = typepigeon.convert_value(value, self.fields[key]) else: self.fields[key] = type(value) self.__configuration[key] = value
def convert_key_pairs(value_mapping: Mapping, type_mapping: Mapping[str, type]) -> Mapping: value_mapping = dict(**value_mapping) keys = list(value_mapping) for key in keys: if key in type_mapping: value = value_mapping[key] value_type = type_mapping[key] if not isinstance(value_type, (Collection)) and issubclass( value_type, Configuration): value = value_type(**value) elif isinstance(value, dict): value = convert_key_pairs(value, value_type) else: value = typepigeon.convert_value(value, value_type) else: value = typepigeon.convert_value(value_mapping, type_mapping) value_mapping[key] = value return value_mapping
def packets(self) -> List[LocationPacket]: if self.__last_access_time is not None and self.interval is not None: interval = datetime.now() - self.__last_access_time if interval < self.interval: raise TimeIntervalError( f"interval {interval} less than minimum interval {self.interval}" ) if Path(self.location).exists(): with open(Path( self.location).expanduser().resolve()) as file_connection: features = geojson.load(file_connection) else: response = requests.get(self.location, stream=True) features = geojson.loads(response.text) packets = [] for feature in features["features"]: if feature["geometry"]["type"] == "Point": properties = feature["properties"] time = typepigeon.convert_value(properties["time"], datetime) del properties["time"] if "from" in properties: from_callsign = properties["from"] to_callsign = properties["to"] del properties["from"], properties["to"] packet = APRSPacket( from_callsign, to_callsign, time, *feature["geometry"]["coordinates"], source=self.location, **properties, ) else: packet = LocationPacket( time, *feature["geometry"]["coordinates"], source=self.location, **properties, ) packets.append(packet) self.__last_access_time = datetime.now() return packets
def parse_record_values(record: Dict[str, Any], field_types: Dict[str, type]) -> Dict[str, Any]: """ parse the values in the given record into their respective field types :param record: dictionary mapping fields to values :param field_types: dictionary mapping fields to types :return: record with values parsed into their respective types """ for field, value in record.items(): if field in field_types: field_type = field_types[field] record[field] = convert_value(value, field_type) return record
def packets(self) -> List[APRSPacket]: if self.__last_access_time is not None and self.interval is not None: interval = datetime.now() - self.__last_access_time if interval < self.interval: raise TimeIntervalError( f"interval {interval} less than minimum interval {self.interval}" ) if Path(self.location).exists(): file_connection = open(Path(self.location).expanduser().resolve()) lines = file_connection.readlines() else: file_connection = requests.get(self.location, stream=True) lines = file_connection.iter_lines() packets = [] for line in lines: if len(line) > 0: if isinstance(line, bytes): line = line.decode() if line not in self.__parsed_lines: self.__parsed_lines.append(line) try: packet_time, raw_aprs = line.split(": ", 1) packet_time = typepigeon.convert_value( packet_time, datetime) except: raw_aprs = line packet_time = datetime.now() raw_aprs = raw_aprs.strip() try: packets.append( APRSPacket.from_frame(raw_aprs, packet_time, source=self.location)) except Exception as error: logging.debug(f"{error.__class__.__name__} - {error}") file_connection.close() if self.callsigns is not None: packets = [ packet for packet in packets if packet.from_callsign in self.callsigns ] self.__last_access_time = datetime.now() return packets
def __init__( self, time: datetime, x: float, y: float, z: float = None, crs: CRS = None, source: str = None, **kwargs, ): if not isinstance(time, datetime): time = typepigeon.convert_value(time, datetime) self.time = time self.coordinates = numpy.array((x, y, z if z is not None else 0)) self.crs = crs if crs is not None else DEFAULT_CRS self.source = source self.attributes = kwargs
def __init__( self, default: PredictionConfiguration, perturbations: List[PredictionConfiguration], **configuration, ): if not isinstance(perturbations, Mapping): perturbations = { f"profile_{index + 1}": perturbation for index, perturbation in enumerate( typepigeon.convert_value(perturbations, [PredictionConfiguration])) } configuration["default"] = default configuration["perturbations"] = perturbations super().__init__(**configuration)
def update(self, other: Mapping): for key, value in other.items(): if key in self: if isinstance(self[key], Configuration): self[key].update(value) else: field_type = self.fields[key] if (isinstance(field_type, type) and issubclass(field_type, Configuration) and value is not None): converted_value = field_type(**value) elif isinstance(field_type, Mapping) and value is not None: converted_value = convert_key_pairs(value, field_type) else: converted_value = typepigeon.convert_value( value, field_type) if self[key] is None or self[key] != converted_value: value = converted_value self[key] = value
def get(self) -> Dict[str, Any]: response = requests.get(self.api_url, params=self.query) if response.status_code == 200: response = response.json() if "error" not in response: # TODO tawhiri currently does not include descent when querying a float profile if self.profile == FlightProfile.float: # this code runs another prediction query with a standard profile and extracts the descent stage to append to the response from the original query for stage in response["prediction"]: # if a descent stage exists, we don't need to do anything if stage["stage"] == "descent": break else: for stage in response["prediction"]: if stage["stage"] == "float": float_end = stage["trajectory"][-1] break else: raise PredictionError( "API did not return a float trajectory") standard_profile_query = self.__class__( start_location=[ float_end["longitude"], float_end["latitude"], float_end["altitude"], ], start_time=typepigeon.convert_value( float_end["datetime"], datetime), ascent_rate=10, burst_altitude=float_end["altitude"] + 0.1, sea_level_descent_rate=self.sea_level_descent_rate, profile=FlightProfile.standard, version=self.version, dataset_time=self.dataset_time, float_altitude=None, float_duration=None, api_url=self.api_url, name=self.name, descent_only=True, ) for stage in standard_profile_query.get( )["prediction"]: if stage["stage"] == "descent": response["prediction"].append(stage) break if self.descent_only: indices_to_remove = [] for index, stage in enumerate(response["prediction"]): # if a descent stage exists, we don't need to do anything if stage["stage"] != "descent": indices_to_remove.append(index) break for index in indices_to_remove: response["prediction"].pop(index) return response else: raise PredictionError(response["error"]["description"]) else: try: error = response.json()["error"]["description"] except: error = "no message" raise ConnectionError( f"connection raised error {response.status_code} for {response.url} - {error}" )
def __init__( self, start_location: Union[Tuple[float, float, Optional[float]], Point], start_time: datetime, ascent_rate: float, burst_altitude: float, sea_level_descent_rate: float, profile: FlightProfile = None, version: float = None, dataset_time: datetime = None, float_altitude: float = None, float_duration: timedelta = None, api_url: PredictionAPIURL = None, name: str = None, descent_only: bool = False, ): if profile is None: if not descent_only and (float_altitude is not None or float_duration is not None): profile = FlightProfile.float else: profile = FlightProfile.standard if dataset_time is not None: if not isinstance(dataset_time, datetime): dataset_time = typepigeon.convert_value(dataset_time, datetime) if (dataset_time.tzinfo is None or dataset_time.tzinfo.utcoffset(dataset_time) is None): dataset_time = UTC_TIMEZONE.localize(dataset_time) if api_url is None: api_url = PredictionAPIURL.cusf if isinstance(api_url, PredictionAPIURL): api_url = api_url.value if name is None: name = "cusf_prediction" super().__init__( api_url, start_location, start_time, ascent_rate, burst_altitude, sea_level_descent_rate, float_altitude, float_duration, name, descent_only, ) # CUSF API requires longitude in 0-360 format if self.start_location.x < 0: x = self.start_location.x + 360 else: x = self.start_location.x launch_coordinates = [x, self.start_location.y] if self.start_location.has_z: launch_coordinates.append(self.start_location.z) self.launch_site = Point(launch_coordinates) self.profile = profile if not self.descent_only else FlightProfile.standard self.version = version self.dataset_time = dataset_time
def end_time(self, end_time: datetime): end_time = typepigeon.convert_value(end_time, datetime) self.__end_time = end_time if self.end_time < self.start_time: self.end_time = self.start_time self.start_time = end_time
def start_time(self, start_time: datetime): start_time = typepigeon.convert_value(start_time, datetime) self.__start_time = start_time if self.start_time > self.end_time: self.start_time = self.end_time self.end_time = start_time