def post(): """Upload a new sector definition""" req_args = utils.parse_args(_PARSER) sector_name = req_args["name"] if not sector_name: return responses.bad_request_resp("Sector name must be provided") sector_json = req_args["content"] if sector_json: sector_element = validate_geojson_sector(sector_json) if not isinstance(sector_element, SectorElement): return responses.bad_request_resp( f"Invalid sector content: {sector_element}" ) else: sector_element = None sector = SectorWrapper(sector_name, sector_element) err = utils.sim_proxy().simulation.load_sector(sector) return responses.checked_resp(err, HTTPStatus.CREATED)
def post(): """Logic for POST events""" req_args = utils.parse_args(_PARSER) if not utils.sim_proxy().simulation.sector: return responses.bad_request_resp( "A sector definition is required before uploading a scenario") # TODO(rkm 2020-01-12) Should never include the file extension name = req_args["name"] if not name: return responses.bad_request_resp("Scenario name must be provided") content = req_args["content"] if content: err = validate_json_scenario(content) if err: return responses.bad_request_resp( f"Invalid scenario content: {err}") scenario = ScenarioWrapper(name, content) err = utils.sim_proxy().simulation.load_scenario(scenario) return responses.checked_resp(err, HTTPStatus.CREATED)
def get(): """Logic for GET events. Returns the current episode ID and log content""" req_args = utils.parse_args(_PARSER) close_ep = req_args.get("close_ep", False) if not in_agent_mode(): return responses.bad_request_resp( "Episode data only recorded when in Agent mode") ep_file_path = bb_logging.EP_FILE if not ep_file_path: return responses.bad_request_resp("No episode being recorded") if close_ep: err = utils.sim_proxy().simulation.reset() if err: return responses.internal_err_resp( f"Couldn't reset simulation: {err}") if not ep_file_path.exists(): return responses.internal_err_resp("Could not find episode file") lines = list(line.rstrip("\n") for line in open(ep_file_path)) data = { "cur_ep_id": bb_logging.EP_ID, "cur_ep_file": str(ep_file_path.absolute()), "log": lines, } return responses.ok_resp(data)
def check_exists(sim_proxy: SimProxy, callsign: types.Callsign, negate: bool = False) -> Optional[Response]: """Checks if an aircraft exists, and returns an appropriate response if not""" exists = sim_proxy.aircraft.exists(callsign) if not isinstance(exists, bool): return responses.internal_err_resp( f"Could not check if the aircraft exists: {exists}") if not exists and not negate: return responses.bad_request_resp( f'Aircraft "{callsign}" does not exist') if exists and negate: return responses.bad_request_resp( f'Aircraft "{callsign}" already exists') return None
def test_pos_get_single(test_flask_client): """Tests the GET method with a single aircraft""" # Test arg parsing endpoint_str = f"{_ENDPOINT_PATH}?{utils.CALLSIGN_LABEL}" callsign = "" resp = test_flask_client.get(f"{endpoint_str}={callsign}") assert resp.status_code == HTTPStatus.BAD_REQUEST assert utils.CALLSIGN_LABEL in resp.json["message"] with mock.patch(patch_utils_path(_ENDPOINT), wraps=utils) as utils_patch: utils_patch.CALLSIGN_LABEL = utils.CALLSIGN_LABEL sim_proxy_mock = mock.Mock() utils_patch.sim_proxy.return_value = sim_proxy_mock # Test error from simulation properties sim_proxy_mock.simulation.properties = "Error" endpoint_str = f"{endpoint_str}=TEST" resp = test_flask_client.get(endpoint_str) assert resp.status_code == HTTPStatus.INTERNAL_SERVER_ERROR assert resp.data.decode() == "Error" # Test aircraft existence check sim_proxy_mock.simulation.properties = TEST_SIM_PROPS with api.FLASK_APP.test_request_context(): utils_patch.check_exists.return_value = bad_request_resp( "Missing aircraft") resp = test_flask_client.get(endpoint_str) assert resp.status_code == HTTPStatus.BAD_REQUEST assert resp.data.decode() == "Missing aircraft" # Test error from aircraft properties utils_patch.check_exists.return_value = None sim_proxy_mock.aircraft.properties.return_value = "Missing properties" resp = test_flask_client.get(endpoint_str) assert resp.status_code == HTTPStatus.INTERNAL_SERVER_ERROR assert resp.data.decode() == "Missing properties" # Test valid response sim_proxy_mock.aircraft.properties.return_value = TEST_AIRCRAFT_PROPS resp = test_flask_client.get(endpoint_str) assert resp.status_code == HTTPStatus.OK assert resp.json == { **utils.convert_aircraft_props(TEST_AIRCRAFT_PROPS), "scenario_time": TEST_SIM_PROPS.scenario_time, }
def post(): """ Logic for POST events. If the request contains valid aircraft information, then a request is sent to the simulator to create it """ req_args = utils.parse_args(_PARSER) callsign = req_args[utils.CALLSIGN_LABEL] resp = utils.check_exists(utils.sim_proxy(), callsign, negate=True) if resp: return resp position_or_resp = utils.try_parse_lat_lon(req_args) if not isinstance(position_or_resp, LatLon): return position_or_resp if not req_args["type"]: return responses.bad_request_resp( "Aircraft type must be specified") err = utils.sim_proxy().aircraft.create( callsign, req_args["type"], position_or_resp, req_args["hdg"], req_args["alt"], req_args["gspd"], ) return responses.checked_resp(err, HTTPStatus.CREATED)
def get(): """ Logic for GET events. If the request contains an identifier to an existing aircraft, then information about its route (FMS flightplan) is returned """ req_args = utils.parse_args(_PARSER) callsign = req_args[utils.CALLSIGN_LABEL] resp = utils.check_exists(utils.sim_proxy(), callsign) if resp: return resp route_info = utils.sim_proxy().aircraft.route(callsign) if not isinstance(route_info, tuple): if route_info == "Aircraft has no route": return responses.bad_request_resp(route_info) return responses.internal_err_resp(route_info) data = { utils.CALLSIGN_LABEL: str(callsign), "route_name": route_info[0], "next_waypoint": route_info[1], "route_waypoints": route_info[2], } return responses.ok_resp(data)
def try_parse_lat_lon(args: dict) -> Union[types.LatLon, Response]: """Attempts to parse a LatLon from an argument dict""" try: assert "lat" in args, "Expected args to contain 'lat'" assert "lon" in args, "Expected args to contain 'lon'" return types.LatLon(args["lat"], args["lon"]) except AssertionError as exc: return bad_request_resp(f"Invalid LatLon: {exc}")
def post(): """Logic for POST events""" if Settings.SIM_MODE != SimMode.Agent: return responses.bad_request_resp( "Must be in agent mode to use step") err = utils.sim_proxy().simulation.step() return responses.checked_resp(err)
def post(): """Logic for post events""" if Settings.SIM_MODE != SimMode.Sandbox: return responses.bad_request_resp( f"Can't resume sim from mode {Settings.SIM_MODE.name}") err = utils.sim_proxy().simulation.resume() return responses.checked_resp(err)
def post(): """Logic for POST events. Sets the seed of the simulator""" req_args = utils.parse_args(_PARSER) seed: int = req_args["value"] if not is_valid_seed(seed): return responses.bad_request_resp( "Invalid seed specified. Must be a positive integer less than 2^32" ) err = utils.sim_proxy().simulation.set_seed(seed) return responses.checked_resp(err)
def post(): """Logic for POST events. Sets the speed multiplier for the simulation""" req_args = utils.parse_args(_PARSER) multiplier = round(req_args["multiplier"], 2) if multiplier <= 0: return responses.bad_request_resp( "Multiplier must be greater than 0") # TODO Check if we still need to keep track of step_dt in the client err = utils.sim_proxy().simulation.set_speed(multiplier) return responses.checked_resp(err)
def get(): """Returns the sector defined in the current simulation""" sector: SectorWrapper = utils.sim_proxy().simulation.sector if not sector: return responses.bad_request_resp("No sector has been set") # TODO (RKM 2019-12-20) Check what exceptions this can throw try: geojson_str = geojson.dumps(sector.element) except Exception as exc: return responses.internal_err_resp(f"Couldn't get sector geojson: {exc}") return responses.ok_resp({"name": sector.name, "content": geojson_str})
def get(): """Logic for GET events""" if not in_agent_mode(): return responses.bad_request_resp( "Episode data only recorded when in Agent mode" ) current_ep_file = bb_logging.EP_FILE if not current_ep_file: return responses.bad_request_resp("No episode being recorded") cwd = os.getcwd() full_log_dir = os.path.join(cwd, bb_logging.INST_LOG_DIR) full_ep_file = os.path.join(cwd, current_ep_file) data = { "inst_id": bb_logging.INSTANCE_ID, "cur_ep_id": bb_logging.EP_ID, "cur_ep_file": full_ep_file, "log_dir": full_log_dir, } return responses.ok_resp(data)
def test_hdg_post(test_flask_client): """Tests the POST method""" # Test arg parsing resp = test_flask_client.post(_ENDPOINT_PATH) assert resp.status_code == HTTPStatus.BAD_REQUEST data = {api_utils.CALLSIGN_LABEL: "FAKE"} resp = test_flask_client.post(_ENDPOINT_PATH, json=data) assert resp.status_code == HTTPStatus.BAD_REQUEST data["hdg"] = "aaa" resp = test_flask_client.post(_ENDPOINT_PATH, json=data) assert resp.status_code == HTTPStatus.BAD_REQUEST with mock.patch(patch_utils_path(_ENDPOINT)) as utils_patch: sim_proxy_mock = mock.Mock() utils_patch.sim_proxy.return_value = sim_proxy_mock # Test aircraft exists check with api.FLASK_APP.test_request_context(): utils_patch.check_exists.return_value = bad_request_resp( "Missing aircraft") data["hdg"] = 123 resp = test_flask_client.post(_ENDPOINT_PATH, json=data) assert resp.status_code == HTTPStatus.BAD_REQUEST assert resp.data.decode() == "Missing aircraft" # Test error from set_heading utils_patch.check_exists.return_value = None sim_proxy_mock.aircraft.set_heading.return_value = "Couldn't set heading" resp = test_flask_client.post(_ENDPOINT_PATH, json=data) assert resp.status_code == HTTPStatus.INTERNAL_SERVER_ERROR assert resp.data.decode() == "Couldn't set heading" # Test valid response sim_proxy_mock.aircraft.set_heading.return_value = None resp = test_flask_client.post(_ENDPOINT_PATH, json=data) assert resp.status_code == HTTPStatus.OK
def get(): """Logic for GET events. Returns properties for the specified aircraft""" req_args = utils.parse_args(_PARSER) callsign = req_args[utils.CALLSIGN_LABEL] sim_props = utils.sim_proxy().simulation.properties if not isinstance(sim_props, SimProperties): return responses.internal_err_resp(sim_props) if callsign: resp = utils.check_exists(utils.sim_proxy(), callsign) if resp: return resp props = utils.sim_proxy().aircraft.properties(callsign) if not isinstance(props, AircraftProperties): return internal_err_resp(props) data = utils.convert_aircraft_props(props) data.update({"scenario_time": sim_props.scenario_time}) return responses.ok_resp(data) # else: get_all_properties props = utils.sim_proxy().aircraft.all_properties if isinstance(props, str): return responses.internal_err_resp( f"Couldn't get the aircraft properties: {props}") if not props: return responses.bad_request_resp("No aircraft in the simulation") data = {} for prop in props.values(): data.update(utils.convert_aircraft_props(prop)) data["scenario_time"] = sim_props.scenario_time return responses.ok_resp(data)
def post(): """ Requests that the specified aircraft proceeds immediately to the specified waypoint """ req_args = utils.parse_args(_PARSER) waypoint_name = req_args["waypoint"] if not waypoint_name: return responses.bad_request_resp( "Waypoint name must be specified") callsign = req_args[utils.CALLSIGN_LABEL] resp = utils.check_exists(utils.sim_proxy(), callsign) if resp: return resp err = utils.sim_proxy().aircraft.direct_to_waypoint( callsign, waypoint_name) return responses.checked_resp(err)
def post(): """ Logic for POST events. Returns the simulator to a previous state given a logfile :return: """ if Settings.SIM_MODE != SimMode.Agent: return responses.bad_request_resp("Can only be used in agent mode") if Settings.SIM_TYPE != SimType.BlueSky: return responses.bad_request_resp( f"Method not supported for the {Settings.SIM_TYPE} simulator") req_args = parse_args(_PARSER) if bool(req_args["filename"]) == bool(req_args["lines"]): return responses.bad_request_resp( "Either filename or lines must be specified") target_time = req_args["time"] if target_time <= 0: return responses.bad_request_resp( "Target time must be greater than 0") props = sim_proxy().simulation.properties if isinstance(props, str): return responses.internal_err_resp( f"Could not get the sim properties: {props}") prev_dt = props.dt _LOGGER.debug("Starting log reload") # Reset now so the current episode log is closed err = sim_proxy().simulation.reset() if err: return responses.internal_err_resp(f"Simulation not reset: {err}") if req_args["filename"]: log_path = Path(os.getcwd(), req_args["filename"]) if not log_path.exists(): return responses.bad_request_resp( f'Could not find episode file {req_args["filename"]}') with open(req_args["filename"]) as log_file: lines = list(log_file) else: lines = req_args["lines"] _LOGGER.debug("Parsing log content") parsed_scn = parse_lines(lines, target_time) if isinstance(parsed_scn, str): return responses.bad_request_resp( f"Could not parse episode content: {parsed_scn}") # TODO Move regex to outer scope and comment # Assert that the requested time is not past the end of the log last_data = next(x for x in reversed(lines) if re.match(r".*A \[(\d+)\] (.*)$", x)) last_time = int(re.search(r"\[(.*)]", last_data).group(1)) if target_time > last_time: return responses.bad_request_resp( "Error: Target time was greater than the latest time in the log" ) # err = validate_scenario(parsed_scn["lines"]) if err: return responses.bad_request_resp( "Could not create a valid scenario from the given log") # All good - do the reload _LOGGER.debug("Setting the simulator seed") err = sim_proxy().simulation.set_seed(int(parsed_scn["seed"])) if err: return responses.internal_err_resp(f"Could not set seed {err}") scn_name = f"reloads/{str(uuid.uuid4())[:8]}.scn" _LOGGER.debug("Uploading the new scenario") # store_local_scn(scn_name, parsed_scn["lines"]) err = sim_proxy().simulation.upload_new_scenario( scn_name, parsed_scn["lines"]) if err: return responses.internal_err_resp( f"Error uploading scenario: {err}") _LOGGER.info("Starting the new scenario") err = sim_proxy().simulation.load_scenario(scn_name, start_paused=True) if err: return responses.internal_err_resp( f"Could not start scenario after upload {err}") props = sim_proxy().simulation.properties if isinstance(props, str): return responses.internal_err_resp( f"Could not get the sim properties: {props}") diff = target_time - props.scenario_time if diff: # Naive approach - set DTMULT to target, then STEP once... _LOGGER.debug( f"Time difference is {diff}. Stepping to {target_time}") err = sim_proxy().simulation.set_speed(diff) if err: return responses.internal_err_resp( f"Could not change speed: {err}") _LOGGER.debug("Performing step") err = sim_proxy().simulation.step() if err: return responses.internal_err_resp( f"Could not step simulations: {err}") else: _LOGGER.debug("Simulation already at required time") # Reset DTMULT to the previous value err = sim_proxy().simulation.set_speed(prev_dt) if err: return responses.internal_err_resp( "Episode reloaded, but could not reset DTMULT to previous value" ) # TODO Do we want to check before/after positions here and check if the # differences are acceptable? return responses.ok_resp()
def test_cre_post(test_flask_client): """Tests the POST method""" # Test arg parsing data = {} resp = test_flask_client.post(_ENDPOINT_PATH, json=data) assert resp.status_code == HTTPStatus.BAD_REQUEST assert utils.CALLSIGN_LABEL in resp.json["message"] callsign = "T" data = {utils.CALLSIGN_LABEL: callsign} resp = test_flask_client.post(_ENDPOINT_PATH, json=data) assert resp.status_code == HTTPStatus.BAD_REQUEST assert utils.CALLSIGN_LABEL in resp.json["message"] callsign = "AAA" data = {utils.CALLSIGN_LABEL: callsign} resp = test_flask_client.post(_ENDPOINT_PATH, json=data) assert resp.status_code == HTTPStatus.BAD_REQUEST assert "type" in resp.json["message"] data["type"] = "" resp = test_flask_client.post(_ENDPOINT_PATH, json=data) assert resp.status_code == HTTPStatus.BAD_REQUEST assert "lat" in resp.json["message"] data["lat"] = 91 resp = test_flask_client.post(_ENDPOINT_PATH, json=data) assert resp.status_code == HTTPStatus.BAD_REQUEST assert "lon" in resp.json["message"] data["lon"] = 181 resp = test_flask_client.post(_ENDPOINT_PATH, json=data) assert resp.status_code == HTTPStatus.BAD_REQUEST assert "hdg" in resp.json["message"] data["hdg"] = "aaa" resp = test_flask_client.post(_ENDPOINT_PATH, json=data) assert resp.status_code == HTTPStatus.BAD_REQUEST assert resp.json["message"]["hdg"] == "Heading must be an int" data["hdg"] = 123 resp = test_flask_client.post(_ENDPOINT_PATH, json=data) assert resp.status_code == HTTPStatus.BAD_REQUEST assert "alt" in resp.json["message"] data["alt"] = -1 resp = test_flask_client.post(_ENDPOINT_PATH, json=data) assert resp.status_code == HTTPStatus.BAD_REQUEST assert resp.json["message"]["alt"] == "Altitude must be positive" data["alt"] = "FL100" resp = test_flask_client.post(_ENDPOINT_PATH, json=data) assert resp.status_code == HTTPStatus.BAD_REQUEST assert "gspd" in resp.json["message"] data["gspd"] = "..." resp = test_flask_client.post(_ENDPOINT_PATH, json=data) assert resp.status_code == HTTPStatus.BAD_REQUEST assert resp.json["message"]["gspd"] == "Ground speed must be numeric" with mock.patch(patch_utils_path(_ENDPOINT), wraps=utils) as utils_patch: utils_patch.CALLSIGN_LABEL = utils.CALLSIGN_LABEL # Test response when aircraft exists with api.FLASK_APP.test_request_context(): response = responses.bad_request_resp("Missing aircraft") utils_patch.check_exists.return_value = response data = { utils.CALLSIGN_LABEL: "TEST1", "type": "", "lat": "1.23", "lon": "4.56", "hdg": 123, "alt": 18_500, "gspd": 50, }
def test_listroute_get(test_flask_client): """Tests the GET method""" endpoint_path = f"{_ENDPOINT_PATH}?{utils.CALLSIGN_LABEL}=" # Test arg parsing resp = test_flask_client.get(_ENDPOINT_PATH) assert resp.status_code == HTTPStatus.BAD_REQUEST callsign_str = "A" resp = test_flask_client.get(f"{endpoint_path}{callsign_str}") assert resp.status_code == HTTPStatus.BAD_REQUEST assert utils.CALLSIGN_LABEL in resp.json["message"] with mock.patch(patch_utils_path(_ENDPOINT), wraps=utils) as utils_patch: utils_patch.CALLSIGN_LABEL = utils.CALLSIGN_LABEL sim_proxy_mock = mock.Mock() utils_patch.sim_proxy.return_value = sim_proxy_mock # Test aircraft exists check with api.FLASK_APP.test_request_context(): utils_patch.check_exists.return_value = bad_request_resp( "Missing aircraft") callsign_str = "TEST" resp = test_flask_client.get(f"{endpoint_path}{callsign_str}") assert resp.status_code == HTTPStatus.BAD_REQUEST assert resp.data.decode() == "Missing aircraft" # Test response when no route defined utils_patch.check_exists.return_value = None sim_proxy_mock.aircraft.route.return_value = "Aircraft has no route" resp = test_flask_client.get(f"{endpoint_path}{callsign_str}") assert resp.status_code == HTTPStatus.BAD_REQUEST assert resp.data.decode() == "Aircraft has no route" # Test error from aircraft route sim_proxy_mock.aircraft.route.return_value = "Couldn't get route" resp = test_flask_client.get(f"{endpoint_path}{callsign_str}") assert resp.status_code == HTTPStatus.INTERNAL_SERVER_ERROR assert resp.data.decode() == "Couldn't get route" # Test valid response route_info = ( "test_route", "FIRE", ["WATER", "FIRE", "EARTH"], ) sim_proxy_mock.aircraft.route.return_value = route_info resp = test_flask_client.get(f"{endpoint_path}{callsign_str}") assert resp.status_code == HTTPStatus.OK assert resp.json == { utils.CALLSIGN_LABEL: "TEST", "route_name": route_info[0], "next_waypoint": route_info[1], "route_waypoints": route_info[2], }