diff --git a/monitoring/atproxy/gunicorn.conf.py b/monitoring/atproxy/gunicorn.conf.py new file mode 100644 index 0000000000..dc5136d28b --- /dev/null +++ b/monitoring/atproxy/gunicorn.conf.py @@ -0,0 +1,21 @@ +import os + +from gunicorn.http import Request +from gunicorn.http.wsgi import Response +from gunicorn.workers.base import Worker +from loguru import logger + + +def pre_request(worker: Worker, req: Request): + """gunicorn server hook called just before a worker processes the request.""" + logger.debug("gunicorn pre_request from worker {} (OS PID {}): {} {}", worker.pid, os.getpid(), req.method, req.path) + + +def post_request(worker: Worker, req: Request, environ: dict, resp: Response): + """gunicorn server hook called after a worker processes the request.""" + logger.debug("gunicorn post_request from worker {} (OS PID {}): {} {} -> {}", worker.pid, os.getpid(), req.method, req.path, resp.status_code) + + +def worker_abort(worker: Worker): + """gunicorn server hook called when a worker received the SIGABRT signal.""" + logger.debug("gunicorn worker_abort from worker {} (OS PID {})", worker.pid, os.getpid()) diff --git a/monitoring/atproxy/start.sh b/monitoring/atproxy/start.sh index d53dfa42e0..0babcdabf3 100755 --- a/monitoring/atproxy/start.sh +++ b/monitoring/atproxy/start.sh @@ -23,4 +23,6 @@ gunicorn \ --workers=4 \ --timeout 60 \ --bind=0.0.0.0:5000 \ + --log-level debug \ + --config ./gunicorn.conf.py \ monitoring.atproxy.app:webapp diff --git a/monitoring/mock_uss/gunicorn.conf.py b/monitoring/mock_uss/gunicorn.conf.py index b998bdd73c..fbb3b140fd 100644 --- a/monitoring/mock_uss/gunicorn.conf.py +++ b/monitoring/mock_uss/gunicorn.conf.py @@ -1,6 +1,9 @@ import os from gunicorn.arbiter import Arbiter +from gunicorn.http import Request +from gunicorn.http.wsgi import Response +from gunicorn.workers.base import Worker from loguru import logger from monitoring.mock_uss import webapp @@ -18,6 +21,36 @@ def when_ready(server: Arbiter): webapp.start_periodic_tasks_daemon() +def pre_request(worker: Worker, req: Request): + """gunicorn server hook called just before a worker processes the request.""" + logger.debug( + "gunicorn pre_request from worker {} (OS PID {}): {} {}", + worker.pid, + os.getpid(), + req.method, + req.path, + ) + + +def post_request(worker: Worker, req: Request, environ: dict, resp: Response): + """gunicorn server hook called after a worker processes the request.""" + logger.debug( + "gunicorn post_request from worker {} (OS PID {}): {} {} -> {}", + worker.pid, + os.getpid(), + req.method, + req.path, + resp.status_code, + ) + + +def worker_abort(worker: Worker): + """gunicorn server hook called when a worker received the SIGABRT signal.""" + logger.debug( + "gunicorn worker_abort from worker {} (OS PID {})", worker.pid, os.getpid() + ) + + def on_exit(server: Arbiter): """gunicorn server hook called just before exiting Gunicorn.""" logger.debug( diff --git a/monitoring/mock_uss/ridsp/routes_injection.py b/monitoring/mock_uss/ridsp/routes_injection.py index 90c067ed24..9c8a58e5ce 100644 --- a/monitoring/mock_uss/ridsp/routes_injection.py +++ b/monitoring/mock_uss/ridsp/routes_injection.py @@ -19,6 +19,8 @@ # Time after the last position report during which the created ISA will still # exist. This value must be at least 60 seconds per NET0610. +from ...monitorlib.rid import RIDVersion + RECENT_POSITIONS_BUFFER = datetime.timedelta(seconds=60.2) @@ -45,32 +47,42 @@ def ridsp_create_test(test_id: str) -> Tuple[str, int]: (t0, t1) = req_body.get_span() t1 += RECENT_POSITIONS_BUFFER rect = req_body.get_rect() - flights_url = "{}/mock/ridsp/v1/uss/flights".format( - webapp.config.get(config.KEY_BASE_URL) - ) + uss_base_url = "{}/mock/ridsp".format(webapp.config.get(config.KEY_BASE_URL)) mutated_isa = mutate.put_isa( - resources.utm_client, rect, t0, t1, flights_url, record.version + area=rect, + start_time=t0, + end_time=t1, + uss_base_url=uss_base_url, + isa_id=record.version, + rid_version=RIDVersion.f3411_19, + utm_client=resources.utm_client, ) - if not mutated_isa.dss_response.success: - logger.error("Unable to create ISA in DSS") - response = ErrorResponse(message="Unable to create ISA in DSS") - response["errors"] = mutated_isa.dss_response.errors + if not mutated_isa.dss_query.success: + errors = "\n".join(mutated_isa.dss_query.errors) + msg = f"Unable to create ISA in DSS ({mutated_isa.dss_query.status_code}): {errors}" + logger.error(msg) + response = ErrorResponse(message=msg) + response["errors"] = mutated_isa.dss_query.errors + response["dss_query"] = mutated_isa.dss_query return flask.jsonify(response), 412 bounds = f"(lat {rect.lat_lo().degrees}, lng {rect.lng_lo().degrees})-(lat {rect.lat_hi().degrees}, lng {rect.lng_hi().degrees})" + isa = mutated_isa.dss_query.isa logger.info( - f"Created ISA {mutated_isa.dss_response.isa.id} from {t0} to {t1} at {bounds}" + f"Created ISA {isa.id} version {isa.version} from {t0} to {t1} at {bounds}" ) - record.isa_version = mutated_isa.dss_response.isa.version + record.isa_version = mutated_isa.dss_query.isa.version for (url, notification) in mutated_isa.notifications.items(): - code = notification.response.status_code + code = notification.query.status_code if code == 200: logger.warning( - f"Notification to {notification.request['url']} incorrectly returned 200 rather than 204" + f"Notification to {notification.query.request.url} incorrectly returned 200 rather than 204" ) elif code != 204: - logger.error( - f"Notification failure {code} to {notification.request['url']}: " - ) + msg = f"Notification failure {code} to {notification.query.request.url}" + logger.error(msg) + response = ErrorResponse(message=msg) + response["query"] = notification.query + return flask.jsonify(response), 412 with db as tx: tx.tests[test_id] = record @@ -91,27 +103,30 @@ def ridsp_delete_test(test_id: str) -> Tuple[str, int]: # Delete ISA from DSS deleted_isa = mutate.delete_isa( - resources.utm_client, record.version, record.isa_version + isa_id=record.version, + isa_version=record.isa_version, + rid_version=RIDVersion.f3411_19, + utm_client=resources.utm_client, ) - if not deleted_isa.dss_response.success: + if not deleted_isa.dss_query.success: logger.error(f"Unable to delete ISA {record.version} from DSS") response = ErrorResponse(message="Unable to delete ISA from DSS") - response["errors"] = deleted_isa.dss_response.errors + response["errors"] = deleted_isa.dss_query.errors return flask.jsonify(response), 412 - logger.info(f"Created ISA {deleted_isa.dss_response.isa.id}") + logger.info(f"Created ISA {deleted_isa.dss_query.isa.id}") + result = ChangeTestResponse(version=record.version, injected_flights=record.flights) for (url, notification) in deleted_isa.notifications.items(): - code = notification.response.status_code + code = notification.query.status_code if code == 200: logger.warning( - f"Notification to {notification.request['url']} incorrectly returned 200 rather than 204" + f"Notification to {notification.query.request.url} incorrectly returned 200 rather than 204" ) elif code != 204: logger.error( - f"Notification failure {code} to {notification.request['url']}: " + f"Notification failure {code} to {notification.query.request.url}" ) + result["query"] = notification.query with db as tx: del tx.tests[test_id] - return flask.jsonify( - ChangeTestResponse(version=record.version, injected_flights=record.flights) - ) + return flask.jsonify(result) diff --git a/monitoring/mock_uss/server.py b/monitoring/mock_uss/server.py index 7520931b5f..4c19dbd7e5 100644 --- a/monitoring/mock_uss/server.py +++ b/monitoring/mock_uss/server.py @@ -19,6 +19,12 @@ MAX_PERIODIC_LATENCY = timedelta(seconds=5) +def _get_trace(e: Exception) -> str: + return "".join( + traceback.format_exception(etype=type(e), value=e, tb=e.__traceback__) + ) + + class TaskTrigger(str, Enum): Setup = "Setup" Shutdown = "Shutdown" @@ -99,11 +105,11 @@ def _run_one_time_tasks(self, trigger: TaskTrigger): tx.task_errors.append(TaskError.from_exception(trigger, e)) if trigger == TaskTrigger.Shutdown: logger.error( - f"{type(e).__name__} error in '{task_name}' on process ID {os.getpid()} while shutting down mock_uss: {str(e)}" + f"{type(e).__name__} error in '{task_name}' on process ID {os.getpid()} while shutting down mock_uss: {str(e)}\n{_get_trace(e)}" ) else: logger.error( - f"Stopping mock_uss due to {type(e).__name__} error in '{task_name}' {trigger} task on process ID {os.getpid()}: {str(e)}" + f"Stopping mock_uss due to {type(e).__name__} error in '{task_name}' {trigger} task on process ID {os.getpid()}: {str(e)}\n{_get_trace(e)}" ) self.stop() return @@ -229,11 +235,8 @@ def _periodic_tasks_daemon_loop(self): dt = MAX_PERIODIC_LATENCY time.sleep(dt.total_seconds()) except Exception as e: - trace = "".join( - traceback.format_exception(etype=type(e), value=e, tb=e.__traceback__) - ) logger.error( - f"Shutting down mock_uss due to {type(e).__name__} error while executing '{task_to_execute}' periodic task: {str(e)}\n{trace}" + f"Shutting down mock_uss due to {type(e).__name__} error while executing '{task_to_execute}' periodic task: {str(e)}\n{_get_trace(e)}" ) with db as tx: tx.task_errors.append(TaskError.from_exception(TaskTrigger.Setup, e)) diff --git a/monitoring/mock_uss/tracer/context.py b/monitoring/mock_uss/tracer/context.py index ff08f9e3cf..c481fed8b4 100644 --- a/monitoring/mock_uss/tracer/context.py +++ b/monitoring/mock_uss/tracer/context.py @@ -116,9 +116,7 @@ def _subscribe( if base_url.endswith("/"): base_url = base_url[0:-1] if monitor_rid: - _subscribe_rid( - resources, base_url + "/tracer/f3411v19/v1/uss/identification_service_areas" - ) + _subscribe_rid(resources, base_url + "/tracer/f3411v19") if monitor_scd: _subscribe_scd(resources, base_url) @@ -138,16 +136,17 @@ def _rid_subscription_id() -> str: RID_SUBSCRIPTION_KEY = "subscribe_ridsubscription" -def _subscribe_rid(resources: ResourceSet, callback_url: str) -> None: +def _subscribe_rid(resources: ResourceSet, uss_base_url: str) -> None: _clear_existing_rid_subscription(resources, "old") - create_result = mutate.rid.put_subscription( - resources.dss_client, - resources.area, - resources.start_time, - resources.end_time, - callback_url, - _rid_subscription_id(), + create_result = mutate.rid.upsert_subscription( + area=resources.area, + start_time=resources.start_time, + end_time=resources.end_time, + uss_base_url=uss_base_url, + subscription_id=_rid_subscription_id(), + rid_version=RIDVersion.f3411_19, + utm_client=resources.dss_client, ) resources.logger.log_new(RID_SUBSCRIPTION_KEY, create_result) if not create_result.success: @@ -168,9 +167,10 @@ def _clear_existing_rid_subscription(resources: ResourceSet, suffix: str) -> Non if existing_result.subscription is not None: del_result = mutate.rid.delete_subscription( - resources.dss_client, - _rid_subscription_id(), - existing_result.subscription.version, + subscription_id=_rid_subscription_id(), + subscription_version=existing_result.subscription.version, + rid_version=RIDVersion.f3411_19, + utm_client=resources.dss_client, ) logfile = resources.logger.log_new( "{}_{}_del".format(RID_SUBSCRIPTION_KEY, suffix), del_result diff --git a/monitoring/mock_uss/tracer/routes.py b/monitoring/mock_uss/tracer/routes.py index 672d86bb95..78bbd3762e 100644 --- a/monitoring/mock_uss/tracer/routes.py +++ b/monitoring/mock_uss/tracer/routes.py @@ -347,4 +347,4 @@ def tracer_catch_all(u_path) -> Tuple[str, int]: label = colored("Bad route", "red") logger.error("{} to {} ({}): {}".format(label, u_path, owner, log_name)) - return RESULT + return f"Path is not a supported endpoint: {u_path}", 404 diff --git a/monitoring/monitorlib/fetch/rid.py b/monitoring/monitorlib/fetch/rid.py index 8c8dee7443..d8f6eb595d 100644 --- a/monitoring/monitorlib/fetch/rid.py +++ b/monitoring/monitorlib/fetch/rid.py @@ -21,14 +21,14 @@ class ISA(ImplicitDict): """Version-independent representation of a F3411 identification service area.""" - v19: Optional[v19.api.IdentificationServiceArea] - v22a: Optional[v22a.api.IdentificationServiceArea] + v19_value: Optional[v19.api.IdentificationServiceArea] = None + v22a_value: Optional[v22a.api.IdentificationServiceArea] = None @property def rid_version(self) -> RIDVersion: - if self.v19 is not None: + if self.v19_value is not None: return RIDVersion.f3411_19 - elif self.v22a is not None: + elif self.v22a_value is not None: return RIDVersion.f3411_22a else: raise ValueError("No valid representation was specified for ISA") @@ -38,21 +38,46 @@ def raw( self, ) -> Union[v19.api.IdentificationServiceArea, v22a.api.IdentificationServiceArea]: if self.rid_version == RIDVersion.f3411_19: - return self.v19 + return self.v19_value elif self.rid_version == RIDVersion.f3411_22a: - return self.v22a + return self.v22a_value else: raise NotImplementedError( f"Cannot retrieve raw ISA using RID version {self.rid_version}" ) + def as_v19(self) -> v19.api.IdentificationServiceArea: + if self.rid_version == RIDVersion.f3411_19: + return self.v19_value + elif self.rid_version == RIDVersion.f3411_22a: + return v19.api.IdentificationServiceArea( + flights_url=self.flights_url, + owner=self.v22a_value.owner, + time_start=self.v22a_value.time_start.value, + time_end=self.v22a_value.time_end.value, + version=self.v22a_value.version, + id=self.v22a_value.id, + ) + else: + raise NotImplementedError( + f"Cannot generate v19 representation of ISA using RID version {self.rid_version}" + ) + + def as_v22a(self) -> v22a.api.IdentificationServiceArea: + if self.rid_version == RIDVersion.f3411_22a: + return self.v22a_value + else: + raise NotImplementedError( + f"Cannot generate v22a representation of ISA using RID version {self.rid_version}" + ) + @property def flights_url(self) -> str: if self.rid_version == RIDVersion.f3411_19: - return self.v19.flights_url + return self.v19_value.flights_url elif self.rid_version == RIDVersion.f3411_22a: flights_path = v22a.api.OPERATIONS[v22a.api.OperationID.SearchFlights].path - return self.v22a.uss_base_url + flights_path + return self.v22a_value.uss_base_url + flights_path else: raise NotImplementedError( f"Cannot retrieve ISA flights URLs using RID version {self.rid_version}" @@ -66,18 +91,22 @@ def owner(self) -> str: def id(self) -> str: return self.raw.id + @property + def version(self) -> str: + return self.raw.version + class Flight(ImplicitDict): """Version-independent representation of a F3411 flight.""" - v19: Optional[v19.api.RIDFlight] - v22a: Optional[v22a.api.RIDFlight] + v19_value: Optional[v19.api.RIDFlight] = None + v22a_value: Optional[v22a.api.RIDFlight] = None @property def rid_version(self) -> RIDVersion: - if self.v19 is not None: + if self.v19_value is not None: return RIDVersion.f3411_19 - elif self.v22a is not None: + elif self.v22a_value is not None: return RIDVersion.f3411_22a else: raise ValueError("No valid representation was specified for flight") @@ -87,9 +116,9 @@ def raw( self, ) -> Union[v19.api.RIDFlight, v22a.api.RIDFlight]: if self.rid_version == RIDVersion.f3411_19: - return self.v19 + return self.v19_value elif self.rid_version == RIDVersion.f3411_22a: - return self.v22a + return self.v22a_value else: raise NotImplementedError( f"Cannot retrieve raw flight using RID version {self.rid_version}" @@ -100,8 +129,8 @@ def id(self) -> str: return self.raw.id def as_v19(self) -> v19.api.RIDFlight: - if self.v19 is not None: - return self.v19 + if self.v19_value is not None: + return self.v19_value else: raise NotImplementedError( f"Conversion to F3411-19 RIDFlight has not yet been implemented for RID version {self.rid_version}" @@ -111,14 +140,14 @@ def as_v19(self) -> v19.api.RIDFlight: class FlightDetails(ImplicitDict): """Version-independent representation of details for a F3411 flight.""" - v19: Optional[v19.api.RIDFlightDetails] - v22a: Optional[v22a.api.RIDFlightDetails] + v19_value: Optional[v19.api.RIDFlightDetails] = None + v22a_value: Optional[v22a.api.RIDFlightDetails] = None @property def rid_version(self) -> RIDVersion: - if self.v19 is not None: + if self.v19_value is not None: return RIDVersion.f3411_19 - elif self.v22a is not None: + elif self.v22a_value is not None: return RIDVersion.f3411_22a else: raise ValueError("No valid representation was specified for flight details") @@ -128,9 +157,9 @@ def raw( self, ) -> Union[v19.api.RIDFlightDetails, v22a.api.RIDFlightDetails]: if self.rid_version == RIDVersion.f3411_19: - return self.v19 + return self.v19_value elif self.rid_version == RIDVersion.f3411_22a: - return self.v22a + return self.v22a_value else: raise NotImplementedError( f"Cannot retrieve raw flight details using RID version {self.rid_version}" @@ -144,14 +173,14 @@ def id(self) -> str: class Subscription(ImplicitDict): """Version-independent representation of a F3411 subscription.""" - v19: Optional[v19.api.Subscription] - v22a: Optional[v22a.api.Subscription] + v19_value: Optional[v19.api.Subscription] = None + v22a_value: Optional[v22a.api.Subscription] = None @property def rid_version(self) -> RIDVersion: - if self.v19 is not None: + if self.v19_value is not None: return RIDVersion.f3411_19 - elif self.v22a is not None: + elif self.v22a_value is not None: return RIDVersion.f3411_22a else: raise ValueError("No valid representation was specified for subscription") @@ -161,9 +190,9 @@ def raw( self, ) -> Union[v19.api.Subscription, v22a.api.Subscription]: if self.rid_version == RIDVersion.f3411_19: - return self.v19 + return self.v19_value elif self.rid_version == RIDVersion.f3411_22a: - return self.v22a + return self.v22a_value else: raise NotImplementedError( f"Cannot retrieve raw subscription using RID version {self.rid_version}" @@ -209,26 +238,20 @@ class FetchedISAs(RIDQuery): @property def _v19_response( self, - ) -> Optional[v19.api.SearchIdentificationServiceAreasResponse]: - try: - return ImplicitDict.parse( - self.v19_query.response.json, - v19.api.SearchIdentificationServiceAreasResponse, - ) - except ValueError: - return None + ) -> v19.api.SearchIdentificationServiceAreasResponse: + return ImplicitDict.parse( + self.v19_query.response.json, + v19.api.SearchIdentificationServiceAreasResponse, + ) @property def _v22a_response( self, - ) -> Optional[v22a.api.SearchIdentificationServiceAreasResponse]: - try: - return ImplicitDict.parse( - self.v22a_query.response.json, - v22a.api.SearchIdentificationServiceAreasResponse, - ) - except ValueError: - return None + ) -> v22a.api.SearchIdentificationServiceAreasResponse: + return ImplicitDict.parse( + self.v22a_query.response.json, + v22a.api.SearchIdentificationServiceAreasResponse, + ) @property def error(self) -> Optional[str]: @@ -240,26 +263,18 @@ def error(self) -> Optional[str]: return "DSS response to search ISAs did not contain valid JSON" if self.rid_version == RIDVersion.f3411_19: - if self._v19_response is None: - try: - ImplicitDict.parse( - self.v19_query.response.json, - v19.api.SearchIdentificationServiceAreasResponse, - ) + try: + if not self._v19_response: return "Unknown error with F3411-19 SearchIdentificationServiceAreasResponse" - except ValueError as e: - return f"Error parsing F3411-19 DSS SearchIdentificationServiceAreasResponse: {str(e)}" + except ValueError as e: + return f"Error parsing F3411-19 DSS SearchIdentificationServiceAreasResponse: {str(e)}" if self.rid_version == RIDVersion.f3411_22a: - if self._v22a_response is None: - try: - ImplicitDict.parse( - self.v22a_query.response.json, - v22a.api.SearchIdentificationServiceAreasResponse, - ) + try: + if not self._v22a_response: return "Unknown error with F3411-22a SearchIdentificationServiceAreasResponse" - except ValueError as e: - return f"Error parsing F3411-22a DSS SearchIdentificationServiceAreasResponse: {str(e)}" + except ValueError as e: + return f"Error parsing F3411-22a DSS SearchIdentificationServiceAreasResponse: {str(e)}" return None @@ -272,9 +287,13 @@ def isas(self) -> Dict[str, ISA]: if not self.success: return {} if self.rid_version == RIDVersion.f3411_19: - return {isa.id: ISA(v19=isa) for isa in self._v19_response.service_areas} + return { + isa.id: ISA(v19_value=isa) for isa in self._v19_response.service_areas + } elif self.rid_version == RIDVersion.f3411_22a: - return {isa.id: ISA(v22a=isa) for isa in self._v22a_response.service_areas} + return { + isa.id: ISA(v22a_value=isa) for isa in self._v22a_response.service_areas + } else: raise NotImplementedError( f"Cannot retrieve ISAs using RID version {self.rid_version}" @@ -345,26 +364,20 @@ class FetchedUSSFlights(RIDQuery): @property def _v19_response( self, - ) -> Optional[v19.api.GetFlightsResponse]: - try: - return ImplicitDict.parse( - self.v19_query.response.json, - v19.api.GetFlightsResponse, - ) - except ValueError: - return None + ) -> v19.api.GetFlightsResponse: + return ImplicitDict.parse( + self.v19_query.response.json, + v19.api.GetFlightsResponse, + ) @property def _v22a_response( self, - ) -> Optional[v22a.api.GetFlightsResponse]: - try: - return ImplicitDict.parse( - self.v22a_query.response.json, - v22a.api.GetFlightsResponse, - ) - except ValueError: - return None + ) -> v22a.api.GetFlightsResponse: + return ImplicitDict.parse( + self.v22a_query.response.json, + v22a.api.GetFlightsResponse, + ) @property def success(self) -> bool: @@ -378,26 +391,18 @@ def errors(self) -> List[str]: return ["Flights response did not include valid JSON"] if self.rid_version == RIDVersion.f3411_19: - if self._v19_response is None: - try: - ImplicitDict.parse( - self.v19_query.response.json, - v19.api.GetFlightsResponse, - ) + try: + if not self._v19_response: return ["Unknown error with F3411-19 GetFlightsResponse"] - except ValueError as e: - return [f"Error parsing F3411-19 USS GetFlightsResponse: {str(e)}"] + except ValueError as e: + return [f"Error parsing F3411-19 USS GetFlightsResponse: {str(e)}"] if self.rid_version == RIDVersion.f3411_22a: - if self._v22a_response is None: - try: - ImplicitDict.parse( - self.v22a_query.response.json, - v22a.api.GetFlightsResponse, - ) + try: + if not self._v22a_response: return ["Unknown error with F3411-22a GetFlightsResponse"] - except ValueError as e: - return [f"Error parsing F3411-22a USS GetFlightsResponse: {str(e)}"] + except ValueError as e: + return [f"Error parsing F3411-22a USS GetFlightsResponse: {str(e)}"] return [] @@ -406,9 +411,9 @@ def flights(self) -> List[Flight]: if not self.success: return [] if self.rid_version == RIDVersion.f3411_19: - return [Flight(v19=f) for f in self._v19_response.flights] + return [Flight(v19_value=f) for f in self._v19_response.flights] elif self.rid_version == RIDVersion.f3411_22a: - return [Flight(v22a=f) for f in self._v22a_response.flights] + return [Flight(v22a_value=f) for f in self._v22a_response.flights] else: raise NotImplementedError( f"Cannot retrieve flights using RID version {self.rid_version}" @@ -474,26 +479,20 @@ class FetchedUSSFlightDetails(RIDQuery): @property def _v19_response( self, - ) -> Optional[v19.api.GetFlightDetailsResponse]: - try: - return ImplicitDict.parse( - self.v19_query.response.json, - v19.api.GetFlightDetailsResponse, - ) - except ValueError: - return None + ) -> v19.api.GetFlightDetailsResponse: + return ImplicitDict.parse( + self.v19_query.response.json, + v19.api.GetFlightDetailsResponse, + ) @property def _v22a_response( self, - ) -> Optional[v22a.api.GetFlightDetailsResponse]: - try: - return ImplicitDict.parse( - self.v22a_query.response.json, - v22a.api.GetFlightDetailsResponse, - ) - except ValueError: - return None + ) -> v22a.api.GetFlightDetailsResponse: + return ImplicitDict.parse( + self.v22a_query.response.json, + v22a.api.GetFlightDetailsResponse, + ) @property def success(self) -> bool: @@ -507,30 +506,22 @@ def errors(self) -> List[str]: return ["Flight details response did not include valid JSON"] if self.rid_version == RIDVersion.f3411_19: - if self._v19_response is None: - try: - ImplicitDict.parse( - self.v19_query.response.json, - v19.api.GetFlightDetailsResponse, - ) + try: + if not self._v19_response: return ["Unknown error with F3411-19 GetFlightDetailsResponse"] - except ValueError as e: - return [ - f"Error parsing F3411-19 USS GetFlightDetailsResponse: {str(e)}" - ] + except ValueError as e: + return [ + f"Error parsing F3411-19 USS GetFlightDetailsResponse: {str(e)}" + ] if self.rid_version == RIDVersion.f3411_22a: - if self._v22a_response is None: - try: - ImplicitDict.parse( - self.v22a_query.response.json, - v22a.api.GetFlightDetailsResponse, - ) + try: + if not self._v22a_response: return ["Unknown error with F3411-22a GetFlightDetailsResponse"] - except ValueError as e: - return [ - f"Error parsing F3411-22a USS GetFlightDetailsResponse: {str(e)}" - ] + except ValueError as e: + return [ + f"Error parsing F3411-22a USS GetFlightDetailsResponse: {str(e)}" + ] return [] @@ -539,9 +530,9 @@ def details(self) -> Optional[FlightDetails]: if not self.success: return None if self.rid_version == RIDVersion.f3411_19: - return FlightDetails(v19=self._v19_response.details) + return FlightDetails(v19_value=self._v19_response.details) elif self.rid_version == RIDVersion.f3411_22a: - return FlightDetails(v22a=self._v22a_response.details) + return FlightDetails(v22a_value=self._v22a_response.details) else: raise NotImplementedError( f"Cannot retrieve flight details using RID version {self.rid_version}" @@ -644,26 +635,20 @@ class FetchedSubscription(RIDQuery): @property def _v19_response( self, - ) -> Optional[v19.api.GetSubscriptionResponse]: - try: - return ImplicitDict.parse( - self.v19_query.response.json, - v19.api.GetSubscriptionResponse, - ) - except ValueError: - return None + ) -> v19.api.GetSubscriptionResponse: + return ImplicitDict.parse( + self.v19_query.response.json, + v19.api.GetSubscriptionResponse, + ) @property def _v22a_response( self, - ) -> Optional[v22a.api.GetSubscriptionResponse]: - try: - return ImplicitDict.parse( - self.v22a_query.response.json, - v22a.api.GetSubscriptionResponse, - ) - except ValueError: - return None + ) -> v22a.api.GetSubscriptionResponse: + return ImplicitDict.parse( + self.v22a_query.response.json, + v22a.api.GetSubscriptionResponse, + ) @property def id(self) -> str: @@ -683,30 +668,20 @@ def errors(self) -> List[str]: return ["Subscription response did not include valid JSON"] if self.rid_version == RIDVersion.f3411_19: - if self._v19_response is None: - try: - ImplicitDict.parse( - self.v19_query.response.json, - v19.api.GetSubscriptionResponse, - ) + try: + if not self._v19_response: return ["Unknown error with F3411-19 GetSubscriptionResponse"] - except ValueError as e: - return [ - f"Error parsing F3411-19 USS GetSubscriptionResponse: {str(e)}" - ] + except ValueError as e: + return [f"Error parsing F3411-19 USS GetSubscriptionResponse: {str(e)}"] if self.rid_version == RIDVersion.f3411_22a: - if self._v22a_response is None: - try: - ImplicitDict.parse( - self.v22a_query.response.json, - v22a.api.GetSubscriptionResponse, - ) + try: + if not self._v22a_response: return ["Unknown error with F3411-22a GetSubscriptionResponse"] - except ValueError as e: - return [ - f"Error parsing F3411-22a USS GetSubscriptionResponse: {str(e)}" - ] + except ValueError as e: + return [ + f"Error parsing F3411-22a USS GetSubscriptionResponse: {str(e)}" + ] return [] @@ -715,9 +690,9 @@ def subscription(self) -> Optional[Subscription]: if not self.success: return None if self.rid_version == RIDVersion.f3411_19: - return Subscription(v19=self._v19_response.subscription) + return Subscription(v19_value=self._v19_response.subscription) elif self.rid_version == RIDVersion.f3411_22a: - return Subscription(v22a=self._v22a_response.subscription) + return Subscription(v22a_value=self._v22a_response.subscription) else: raise NotImplementedError( f"Cannot retrieve subscription using RID version {self.rid_version}" diff --git a/monitoring/monitorlib/mutate/rid.py b/monitoring/monitorlib/mutate/rid.py index a17490720b..e5f2d8f794 100644 --- a/monitoring/monitorlib/mutate/rid.py +++ b/monitoring/monitorlib/mutate/rid.py @@ -1,22 +1,41 @@ import datetime -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union from implicitdict import ImplicitDict import s2sphere -from uas_standards.astm.f3411.v19.api import ( - IdentificationServiceArea, - SubscriberToNotify, -) -from uas_standards.astm.f3411.v19.constants import Scope + +from monitoring.monitorlib.fetch.rid import RIDQuery, Subscription, ISA +from monitoring.monitorlib.rid import RIDVersion +from uas_standards.astm.f3411 import v19, v22a +import uas_standards.astm.f3411.v19.api +import uas_standards.astm.f3411.v19.constants +import uas_standards.astm.f3411.v22a.api +import uas_standards.astm.f3411.v22a.constants import yaml from yaml.representer import Representer -from monitoring.monitorlib import fetch, infrastructure, rid_v1 +from monitoring.monitorlib import fetch, infrastructure, rid_v1, rid_v2 + +class ChangedSubscription(RIDQuery): + """Version-independent representation of a subscription following a change in the DSS.""" -class MutatedSubscription(fetch.Query): mutation: Optional[str] = None + @property + def _v19_response(self) -> v19.api.PutSubscriptionResponse: + return ImplicitDict.parse( + self.v19_query.response.json, + v19.api.PutSubscriptionResponse, + ) + + @property + def _v22a_response(self) -> v22a.api.PutSubscriptionResponse: + return ImplicitDict.parse( + self.v22a_query.response.json, + v22a.api.PutSubscriptionResponse, + ) + @property def success(self) -> bool: return not self.errors @@ -24,82 +43,262 @@ def success(self) -> bool: @property def errors(self) -> List[str]: if self.status_code != 200: - return [ - "Failed to {} RID Subscription ({})".format( - self.mutation, self.status_code - ) - ] - if self.json_result is None: - return ["Response did not contain valid JSON"] - sub = self.subscription - if sub is None or not sub.valid: - return ["Response returned an invalid Subscription"] + return ["Failed to mutate subscription ({})".format(self.status_code)] + if self.query.response.json is None: + return ["Subscription response did not include valid JSON"] + + if self.rid_version == RIDVersion.f3411_19: + try: + value = self._v19_response + if not value: + return ["Unknown error with F3411-19 PutSubscriptionResponse"] + except ValueError as e: + return [f"Error parsing F3411-19 USS PutSubscriptionResponse: {str(e)}"] + + if self.rid_version == RIDVersion.f3411_22a: + try: + value = self._v22a_response + if not value: + return ["Unknown error with F3411-22a PutSubscriptionResponse"] + except ValueError as e: + return [ + f"Error parsing F3411-22a USS PutSubscriptionResponse: {str(e)}" + ] + + return [] @property - def subscription(self) -> Optional[rid_v1.Subscription]: - if self.json_result is None: - return None - sub = self.json_result.get("subscription", None) - if not sub: + def subscription(self) -> Optional[Subscription]: + if not self.success: return None - return rid_v1.Subscription(sub) - + if self.rid_version == RIDVersion.f3411_19: + return Subscription(v19_value=self._v19_response.subscription) + elif self.rid_version == RIDVersion.f3411_22a: + return Subscription(v22a_value=self._v22a_response.subscription) + else: + raise NotImplementedError( + f"Cannot retrieve subscription using RID version {self.rid_version}" + ) -yaml.add_representer(MutatedSubscription, Representer.represent_dict) - -def put_subscription( - utm_client: infrastructure.UTMClientSession, +def upsert_subscription( area: s2sphere.LatLngRect, start_time: datetime.datetime, end_time: datetime.datetime, - callback_url: str, + uss_base_url: str, subscription_id: str, + rid_version: RIDVersion, + utm_client: infrastructure.UTMClientSession, subscription_version: Optional[str] = None, -) -> MutatedSubscription: - body = { - "extents": { - "spatial_volume": { - "footprint": {"vertices": rid_v1.vertices_from_latlng_rect(area)}, - "altitude_lo": 0, - "altitude_hi": 3048, +) -> ChangedSubscription: + mutation = "create" if subscription_version is None else "update" + if rid_version == RIDVersion.f3411_19: + body = { + "extents": { + "spatial_volume": { + "footprint": {"vertices": rid_v1.vertices_from_latlng_rect(area)}, + "altitude_lo": 0, + "altitude_hi": 3048, + }, + "time_start": start_time.strftime(rid_v1.DATE_FORMAT), + "time_end": end_time.strftime(rid_v1.DATE_FORMAT), + }, + "callbacks": { + "identification_service_area_url": uss_base_url + + v19.api.OPERATIONS[ + v19.api.OperationID.PostIdentificationServiceArea + ].path[: -len("/{id}")] + }, + } + if subscription_version is None: + op = v19.api.OPERATIONS[v19.api.OperationID.CreateSubscription] + url = op.path.format(id=subscription_id) + else: + op = v19.api.OPERATIONS[v19.api.OperationID.UpdateSubscription] + url = op.path.format(id=subscription_id, version=subscription_version) + return ChangedSubscription( + mutation=mutation, + v19_query=fetch.query_and_describe( + utm_client, op.verb, url, json=body, scope=v19.constants.Scope.Read + ), + ) + elif rid_version == RIDVersion.f3411_22a: + body = { + "extents": { + "volume": { + "outline_polygon": rid_v2.make_polygon_outline(area), + "altitude_lower": rid_v2.make_altitude(0), + "altitude_upper": rid_v2.make_altitude(3048), + }, + "time_start": rid_v2.make_time(start_time), + "time_end": rid_v2.make_time(end_time), }, - "time_start": start_time.strftime(rid_v1.DATE_FORMAT), - "time_end": end_time.strftime(rid_v1.DATE_FORMAT), - }, - "callbacks": {"identification_service_area_url": callback_url}, - } - if subscription_version is None: - url = "/v1/dss/subscriptions/{}".format(subscription_id) + "uss_base_url": uss_base_url, + } + if subscription_version is None: + op = v22a.api.OPERATIONS[v22a.api.OperationID.CreateSubscription] + url = op.path.format(id=subscription_id) + else: + op = v22a.api.OPERATIONS[v22a.api.OperationID.UpdateSubscription] + url = op.path.format(id=subscription_id, version=subscription_version) + return ChangedSubscription( + mutation=mutation, + v22a_query=fetch.query_and_describe( + utm_client, + op.verb, + url, + json=body, + scope=v22a.constants.Scope.DisplayProvider, + ), + ) else: - url = "/v1/dss/subscriptions/{}/{}".format( - subscription_id, subscription_version + raise NotImplementedError( + f"Cannot upsert subscription using RID version {rid_version}" ) - result = MutatedSubscription( - fetch.query_and_describe(utm_client, "PUT", url, json=body, scope=Scope.Read) - ) - result.mutation = "create" if subscription_version is None else "update" - return result def delete_subscription( - utm_client: infrastructure.UTMClientSession, subscription_id: str, subscription_version: str, -) -> MutatedSubscription: - url = "/v1/dss/subscriptions/{}/{}".format(subscription_id, subscription_version) - result = MutatedSubscription( - fetch.query_and_describe(utm_client, "DELETE", url, scope=Scope.Read) - ) - result.mutation = "delete" - return result + rid_version: RIDVersion, + utm_client: infrastructure.UTMClientSession, +) -> ChangedSubscription: + if rid_version == RIDVersion.f3411_19: + op = v19.api.OPERATIONS[v19.api.OperationID.DeleteSubscription] + url = op.path.format(id=subscription_id, version=subscription_version) + return ChangedSubscription( + mutation="delete", + v19_query=fetch.query_and_describe( + utm_client, op.verb, url, scope=v19.constants.Scope.Read + ), + ) + elif rid_version == RIDVersion.f3411_22a: + op = v22a.api.OPERATIONS[v22a.api.OperationID.DeleteSubscription] + url = op.path.format(id=subscription_id, version=subscription_version) + return ChangedSubscription( + mutation="delete", + v22a_query=fetch.query_and_describe( + utm_client, op.verb, url, scope=v22a.constants.Scope.DisplayProvider + ), + ) + else: + raise NotImplementedError( + f"Cannot delete subscription using RID version {rid_version}" + ) -class MutatedISAResponse(fetch.Query): - """Response to a call to the DSS to mutate an ISA""" +class ISAChangeNotification(RIDQuery): + """Version-independent representation of response to a USS notification following an ISA change in the DSS.""" + + @property + def success(self) -> bool: + # Tolerate not-strictly-correct 200 response + return self.status_code == 204 or self.status_code == 200 + + +class SubscriberToNotify(ImplicitDict): + """Version-independent representation of a subscriber to notify of a change in the DSS.""" + + v19_value: Optional[v19.api.SubscriberToNotify] = None + v22a_value: Optional[v22a.api.SubscriberToNotify] = None + + @property + def rid_version(self) -> RIDVersion: + if self.v19_value is not None: + return RIDVersion.f3411_19 + elif self.v22a_value is not None: + return RIDVersion.f3411_22a + else: + raise ValueError( + "No valid representation was specified for SubscriberToNotify" + ) + + @property + def raw( + self, + ) -> Union[v19.api.SubscriberToNotify, v22a.api.SubscriberToNotify]: + if self.rid_version == RIDVersion.f3411_19: + return self.v19_value + elif self.rid_version == RIDVersion.f3411_22a: + return self.v22a_value + else: + raise NotImplementedError( + f"Cannot retrieve raw subscriber to notify using RID version {self.rid_version}" + ) + + def notify( + self, + isa_id: str, + utm_session: infrastructure.UTMClientSession, + isa: Optional[ISA] = None, + ) -> ISAChangeNotification: + # Note that optional `extents` are not specified + if self.rid_version == RIDVersion.f3411_19: + body = { + "subscriptions": self.v19_value.subscriptions, + } + if isa is not None: + body["service_area"] = isa.as_v19() + url = self.v19_value.url + "/" + isa_id + return ISAChangeNotification( + v19_query=fetch.query_and_describe( + utm_session, + "POST", + url, + json=body, + scope=v19.constants.Scope.Write, + ) + ) + elif self.rid_version == RIDVersion.f3411_22a: + body = { + "subscriptions": self.v22a_value.subscriptions, + } + if isa is not None: + body["service_area"] = isa.as_v22a() + op = v22a.api.OPERATIONS[v22a.api.OperationID.PostIdentificationServiceArea] + url = self.v22a_value.url + op.path.format(id=isa_id) + return ISAChangeNotification( + v22a_query=fetch.query_and_describe( + utm_session, + op.verb, + url, + json=body, + scope=v22a.constants.Scope.ServiceProvider, + ) + ) + else: + raise NotImplementedError( + f"Cannot notify subscriber using RID version {self.rid_version}" + ) + + @property + def url(self) -> str: + return self.raw.url + + +class ChangedISA(RIDQuery): + """Version-independent representation of a changed F3411 identification service area.""" mutation: Optional[str] = None + @property + def _v19_response( + self, + ) -> v19.api.PutIdentificationServiceAreaResponse: + return ImplicitDict.parse( + self.v19_query.response.json, + v19.api.PutIdentificationServiceAreaResponse, + ) + + @property + def _v22a_response( + self, + ) -> v22a.api.PutIdentificationServiceAreaResponse: + return ImplicitDict.parse( + self.v22a_query.response.json, + v22a.api.PutIdentificationServiceAreaResponse, + ) + @property def success(self) -> bool: return not self.errors @@ -107,118 +306,193 @@ def success(self) -> bool: @property def errors(self) -> List[str]: if self.status_code != 200: - return ["Failed to {} RID ISA ({})".format(self.mutation, self.status_code)] - if self.json_result is None: - return ["Response did not contain valid JSON"] - try: - _ = self.isa - except ValueError as e: - return ["Response returned an invalid ISA: {}".format(e)] + return ["Failed to mutate ISA ({})".format(self.status_code)] + if self.query.response.json is None: + return ["ISA response did not include valid JSON"] + + if self.rid_version == RIDVersion.f3411_19: + try: + value = self._v19_response + if not value: + return [ + "Unknown error with F3411-19 PutIdentificationServiceAreaResponse" + ] + except ValueError as e: + return [ + f"Error parsing F3411-19 USS PutIdentificationServiceAreaResponse: {str(e)}" + ] + + if self.rid_version == RIDVersion.f3411_22a: + try: + value = self._v22a_response + if not value: + return [ + "Unknown error with F3411-22a PutIdentificationServiceAreaResponse" + ] + except ValueError as e: + return [ + f"Error parsing F3411-22a USS PutIdentificationServiceAreaResponse: {str(e)}" + ] + + return [] @property - def isa(self) -> IdentificationServiceArea: - if self.json_result is None: - raise ValueError("No JSON result present in response from DSS") - isa_dict = self.json_result.get("service_area", None) - if not isa_dict: - raise ValueError("No `service_area` field present in response from DSS") - return IdentificationServiceArea(isa_dict) + def isa(self) -> ISA: + if self.rid_version == RIDVersion.f3411_19: + return ISA(v19_value=self._v19_response.service_area) + elif self.rid_version == RIDVersion.f3411_22a: + return ISA(v22a_value=self._v22a_response.service_area) + else: + raise NotImplementedError( + f"Cannot retrieve ISA using RID version {self.rid_version}" + ) @property def subscribers(self) -> List[SubscriberToNotify]: - if self.json_result is None: - raise ValueError("No JSON result present in response from DSS") - subs = self.json_result.get("subscribers", None) - if not subs: - return [] - return [SubscriberToNotify(sub) for sub in subs] - + if self.rid_version == RIDVersion.f3411_19: + return [ + SubscriberToNotify(v19_value=sub) + for sub in self._v19_response.subscribers + ] + elif self.rid_version == RIDVersion.f3411_22a: + return [ + SubscriberToNotify(v22a_value=sub) + for sub in self._v22a_response.subscribers + ] + else: + raise NotImplementedError( + f"Cannot retrieve subscribers to notify using RID version {self.rid_version}" + ) -yaml.add_representer(MutatedISAResponse, Representer.represent_dict) +class ISAChange(ImplicitDict): + """Result of an attempt to change an ISA (including DSS & notifications)""" -class MutatedISA(ImplicitDict): - """Result of an attempt to mutate an ISA (including DSS & notifications)""" + dss_query: ChangedISA - dss_response: MutatedISAResponse - notifications: Dict[str, fetch.Query] + notifications: Dict[str, ISAChangeNotification] + """Mapping from USS base URL to change notification query""" def put_isa( - utm_client: infrastructure.UTMClientSession, area: s2sphere.LatLngRect, start_time: datetime.datetime, end_time: datetime.datetime, - flights_url: str, - entity_id: str, + uss_base_url: str, + isa_id: str, + rid_version: RIDVersion, + utm_client: infrastructure.UTMClientSession, isa_version: Optional[str] = None, -) -> MutatedISA: - extents = { - "spatial_volume": { - "footprint": {"vertices": rid_v1.vertices_from_latlng_rect(area)}, - "altitude_lo": 0, - "altitude_hi": 3048, - }, - "time_start": start_time.strftime(rid_v1.DATE_FORMAT), - "time_end": end_time.strftime(rid_v1.DATE_FORMAT), - } - body = { - "extents": extents, - "flights_url": flights_url, - } - if isa_version is None: - url = "/v1/dss/identification_service_areas/{}".format(entity_id) - else: - url = "/v1/dss/identification_service_areas/{}/{}".format( - entity_id, isa_version +) -> ISAChange: + mutation = "create" if isa_version is None else "update" + if rid_version == RIDVersion.f3411_19: + body = { + "extents": { + "spatial_volume": { + "footprint": {"vertices": rid_v1.vertices_from_latlng_rect(area)}, + "altitude_lo": 0, + "altitude_hi": 3048, + }, + "time_start": start_time.strftime(rid_v1.DATE_FORMAT), + "time_end": end_time.strftime(rid_v1.DATE_FORMAT), + }, + "flights_url": uss_base_url + + v19.api.OPERATIONS[v19.api.OperationID.SearchFlights].path, + } + if isa_version is None: + op = v19.api.OPERATIONS[v19.api.OperationID.CreateIdentificationServiceArea] + url = op.path.format(id=isa_id) + else: + op = v19.api.OPERATIONS[v19.api.OperationID.UpdateIdentificationServiceArea] + url = op.path.format(id=isa_id, version=isa_version) + dss_response = ChangedISA( + mutation=mutation, + v19_query=fetch.query_and_describe( + utm_client, op.verb, url, json=body, scope=v19.constants.Scope.Write + ), ) - dss_response = MutatedISAResponse( - fetch.query_and_describe(utm_client, "PUT", url, json=body, scope=Scope.Write) - ) - dss_response["mutation"] = "create" if isa_version is None else "update" - - # Notify subscribers - notifications: Dict[str, fetch.Query] = {} - try: - subscribers = dss_response.subscribers - isa = dss_response.isa - except ValueError: - subscribers = [] - isa = None - for subscriber in subscribers: + elif rid_version == RIDVersion.f3411_22a: body = { - "service_area": isa, - "subscriptions": subscriber.subscriptions, - "extents": extents, + "extents": { + "volume": { + "outline_polygon": rid_v2.make_polygon_outline(area), + "altitude_lower": rid_v2.make_altitude(0), + "altitude_upper": rid_v2.make_altitude(3048), + }, + "time_start": rid_v2.make_time(start_time), + "time_end": rid_v2.make_time(end_time), + }, + "uss_base_url": uss_base_url, } - url = "{}/{}".format(subscriber.url, entity_id) - notifications[subscriber.url] = fetch.query_and_describe( - utm_client, "POST", url, json=body, scope=Scope.Write + if isa_version is None: + op = v22a.api.OPERATIONS[v22a.api.OperationID.CreateSubscription] + url = op.path.format(id=isa_id) + else: + op = v22a.api.OPERATIONS[v22a.api.OperationID.UpdateSubscription] + url = op.path.format(id=isa_id, version=isa_version) + dss_response = ChangedISA( + mutation=mutation, + v22a_query=fetch.query_and_describe( + utm_client, + op.verb, + url, + json=body, + scope=v22a.constants.Scope.ServiceProvider, + ), ) + else: + raise NotImplementedError(f"Cannot upsert ISA using RID version {rid_version}") + + if dss_response.success: + isa = dss_response.isa + notifications = { + sub.url: sub.notify(isa.id, utm_client, isa) + for sub in dss_response.subscribers + } + else: + notifications = {} - return MutatedISA(dss_response=dss_response, notifications=notifications) + return ISAChange(dss_query=dss_response, notifications=notifications) def delete_isa( - utm_client: infrastructure.UTMClientSession, entity_id: str, isa_version: str -) -> MutatedISA: - url = "/v1/dss/identification_service_areas/{}/{}".format(entity_id, isa_version) - dss_response = MutatedISAResponse( - fetch.query_and_describe(utm_client, "DELETE", url, scope=Scope.Write) - ) - dss_response["mutation"] = "delete" - - # Notify subscribers - notifications: Dict[str, fetch.Query] = {} - try: - subscribers = dss_response.subscribers - except ValueError: - subscribers = [] - for subscriber in subscribers: - body = {"subscriptions": subscriber.subscriptions} - url = "{}/{}".format(subscriber.url, entity_id) - notifications[subscriber.url] = fetch.query_and_describe( - utm_client, "POST", url, json=body, scope=Scope.Write + isa_id: str, + isa_version: str, + rid_version: RIDVersion, + utm_client: infrastructure.UTMClientSession, +) -> ISAChange: + if rid_version == RIDVersion.f3411_19: + op = v19.api.OPERATIONS[v19.api.OperationID.DeleteIdentificationServiceArea] + url = op.path.format(id=isa_id, version=isa_version) + dss_response = ChangedISA( + mutation="delete", + v19_query=fetch.query_and_describe( + utm_client, op.verb, url, scope=v19.constants.Scope.Write + ), + ) + elif rid_version == RIDVersion.f3411_22a: + op = v22a.api.OPERATIONS[v22a.api.OperationID.UpdateSubscription] + url = op.path.format(id=isa_id, version=isa_version) + dss_response = ChangedISA( + mutation="delete", + v22a_query=fetch.query_and_describe( + utm_client, op.verb, url, scope=v22a.constants.Scope.ServiceProvider + ), ) + else: + raise NotImplementedError(f"Cannot delete ISA using RID version {rid_version}") + + if dss_response.success: + isa = dss_response.isa + notifications = { + sub.url: sub.notify(isa.id, utm_client) for sub in dss_response.subscribers + } + else: + notifications = {} + + return ISAChange(dss_query=dss_response, notifications=notifications) + - return MutatedISA(dss_response=dss_response, notifications=notifications) +yaml.add_representer(ChangedSubscription, Representer.represent_dict) +yaml.add_representer(ChangedISA, Representer.represent_dict) +yaml.add_representer(ISAChange, Representer.represent_dict) diff --git a/monitoring/monitorlib/rid_v2.py b/monitoring/monitorlib/rid_v2.py index e10dce7d0c..3285e3d481 100644 --- a/monitoring/monitorlib/rid_v2.py +++ b/monitoring/monitorlib/rid_v2.py @@ -1,6 +1,7 @@ import datetime -from uas_standards.astm.f3411.v22a.api import Time, Altitude +import s2sphere +from uas_standards.astm.f3411.v22a.api import Time, Altitude, Polygon, LatLngPoint from . import rid_v1 as rid_v1 @@ -13,5 +14,16 @@ def make_altitude(altitude_meters: float) -> Altitude: return Altitude(reference="W84", units="M", value=altitude_meters) +def make_polygon_outline(area: s2sphere.LatLngRect) -> Polygon: + return Polygon( + vertices=[ + LatLngPoint(lat=area.lat_lo().degrees, lng=area.lng_lo().degrees), + LatLngPoint(lat=area.lat_lo().degrees, lng=area.lng_hi().degrees), + LatLngPoint(lat=area.lat_hi().degrees, lng=area.lng_hi().degrees), + LatLngPoint(lat=area.lat_hi().degrees, lng=area.lng_lo().degrees), + ] + ) + + DATE_FORMAT = rid_v1.DATE_FORMAT geo_polygon_string = rid_v1.geo_polygon_string diff --git a/requirements.txt b/requirements.txt index 705685e1a6..f6bd80e4d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,7 +13,7 @@ Flask-HTTPAuth==4.7.0 # atproxy geojson===2.5.0 # uss_qualifier google-auth==1.6.3 graphviz==0.20.1 # uss_qualifier -gunicorn==20.0.4 +gunicorn==20.1.0 implicitdict==2.1.0 itsdangerous==2.0.1 # Version 2.1.0 is not compatible with flask 1.1.2. Jinja2==3.0.3 # See https://github.com/interuss/dss/issues/745