From 1bd9f4b8f650aca042820ec6bec45cfc05089d52 Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Wed, 13 May 2026 19:28:51 +0000 Subject: [PATCH 1/4] Add query type information to mock_uss handlers --- monitoring/mock_uss/f3548v21/routes_scd.py | 6 ++++ .../mock_uss/interaction_logging/logger.py | 19 ++++++++--- monitoring/mock_uss/logging.py | 32 +++++++++++++++++++ monitoring/mock_uss/riddp/routes_riddp_v19.py | 4 ++- .../mock_uss/riddp/routes_riddp_v22a.py | 4 ++- monitoring/mock_uss/ridsp/routes_ridsp_v19.py | 15 +++------ .../mock_uss/ridsp/routes_ridsp_v22a.py | 15 +++------ 7 files changed, 67 insertions(+), 28 deletions(-) diff --git a/monitoring/mock_uss/f3548v21/routes_scd.py b/monitoring/mock_uss/f3548v21/routes_scd.py index a687f9dc73..24da0d4cae 100644 --- a/monitoring/mock_uss/f3548v21/routes_scd.py +++ b/monitoring/mock_uss/f3548v21/routes_scd.py @@ -20,15 +20,18 @@ op_intent_from_flightrecord, ) from monitoring.mock_uss.flights.database import FlightRecord, db +from monitoring.mock_uss.logging import query_type from monitoring.mock_uss.user_interactions.notifications import ( UserNotification, UserNotificationType, ) from monitoring.monitorlib import scd from monitoring.monitorlib.clients.flight_planning.planning import Conflict +from monitoring.monitorlib.fetch import QueryType @webapp.route("/mock/scd/uss/v1/operational_intents/", methods=["GET"]) +@query_type(QueryType.F3548v21USSGetOperationalIntentDetails) @requires_scope(scd.SCOPE_SC) def scdsc_get_operational_intent_details(entityid: str): """Implements getOperationalIntentDetails in ASTM SCD API.""" @@ -62,6 +65,7 @@ def scdsc_get_operational_intent_details(entityid: str): @webapp.route( "/mock/scd/uss/v1/operational_intents//telemetry", methods=["GET"] ) +@query_type(QueryType.F3548v21USSGetOperationalIntentTelemetry) @requires_scope(scd.SCOPE_CM_SA) def scdsc_get_operational_intent_telemetry(entityid: str): """Implements getOperationalIntentTelemetry in ASTM SCD API.""" @@ -110,6 +114,7 @@ def scdsc_get_operational_intent_telemetry(entityid: str): @webapp.route("/mock/scd/uss/v1/operational_intents", methods=["POST"]) +@query_type(QueryType.F3548v21USSNotifyOperationalIntentDetailsChanged) @requires_scope(scd.SCOPE_SC) def scdsc_notify_operational_intent_details_changed(): """Implements notifyOperationalIntentDetailsChanged in ASTM SCD API.""" @@ -146,6 +151,7 @@ def scdsc_notify_operational_intent_details_changed(): @webapp.route("/mock/scd/uss/v1/reports", methods=["POST"]) +@query_type(QueryType.F3548v21USSMakeUssReport) @requires_scope( [scd.SCOPE_SC, scd.SCOPE_CP, scd.SCOPE_CM, scd.SCOPE_CM_SA, scd.SCOPE_AA] ) diff --git a/monitoring/mock_uss/interaction_logging/logger.py b/monitoring/mock_uss/interaction_logging/logger.py index 6a289a5ffe..bf1729aeaa 100644 --- a/monitoring/mock_uss/interaction_logging/logger.py +++ b/monitoring/mock_uss/interaction_logging/logger.py @@ -6,6 +6,7 @@ from monitoring.mock_uss.app import require_config_value, webapp from monitoring.mock_uss.interaction_logging.config import KEY_INTERACTIONS_LOG_DIR +from monitoring.mock_uss.logging import get_query_type from monitoring.monitorlib.clients import QueryHook, query_hooks from monitoring.monitorlib.clients.mock_uss.interactions import ( Interaction, @@ -76,12 +77,22 @@ def interaction_log_after_request(response): elapsed_s = ( datetime.datetime.now(datetime.UTC) - flask.current_app.custom_profiler["start"] ).total_seconds() + query_type = get_query_type() # TODO: Make this configurable instead of hardcoding exactly these query types - if ( - "/uss/v1/" in flask.request.url - or "/uss/identification_service_areas/" in flask.request.url - or "/uss/flights" in flask.request.url + if query_type in ( + QueryType.F3548v21USSGetConstraintDetails, + QueryType.F3548v21USSGetOperationalIntentDetails, + QueryType.F3548v21USSGetOperationalIntentTelemetry, + QueryType.F3548v21USSMakeUssReport, + QueryType.F3548v21USSNotifyConstraintDetailsChanged, + QueryType.F3548v21USSNotifyOperationalIntentDetailsChanged, + QueryType.F3411v22aUSSSearchFlights, + QueryType.F3411v22aUSSPostIdentificationServiceArea, + QueryType.F3411v19USSSearchFlights, + QueryType.F3411v19USSPostIdentificationServiceArea, ): query = describe_flask_query(flask.request, response, elapsed_s) + if query_type: + query.query_type = query_type log_interaction(QueryDirection.Incoming, query) return response diff --git a/monitoring/mock_uss/logging.py b/monitoring/mock_uss/logging.py index 142fb3cfb9..fdf7d3fd76 100644 --- a/monitoring/mock_uss/logging.py +++ b/monitoring/mock_uss/logging.py @@ -2,6 +2,7 @@ import json import os +from functools import wraps import flask import loguru @@ -9,6 +10,7 @@ from loguru import logger from monitoring.mock_uss.app import webapp +from monitoring.monitorlib.fetch import QueryType def _get_request_id(req: Request) -> str: @@ -96,3 +98,33 @@ def end_request_log(e: BaseException | None) -> None: request_logger = get_request_logger() if request_logger is not None: request_logger.remove_from_loguru() + + +def query_type(q_type: QueryType): + """Decorator for a view function that indicates which QueryType it handles.""" + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + return func(*args, **kwargs) + + # Attach the attribute directly to the wrapper function object + setattr(wrapper, "query_type", q_type) + return wrapper + + return decorator + + +def get_query_type() -> QueryType | None: + """Get the query type handled by the active Flask view function (as indicated by the query_type decorator).""" + if ( + flask.request.endpoint + and flask.request.endpoint in flask.current_app.view_functions + ): + handler_func = flask.current_app.view_functions[flask.request.endpoint] + + q_type = getattr(handler_func, "query_type", None) + + return q_type + else: + return None diff --git a/monitoring/mock_uss/riddp/routes_riddp_v19.py b/monitoring/mock_uss/riddp/routes_riddp_v19.py index 6ed77b887c..bdcf020daf 100644 --- a/monitoring/mock_uss/riddp/routes_riddp_v19.py +++ b/monitoring/mock_uss/riddp/routes_riddp_v19.py @@ -10,8 +10,9 @@ from monitoring.mock_uss.app import webapp from monitoring.mock_uss.auth import requires_scope +from monitoring.mock_uss.logging import query_type from monitoring.mock_uss.riddp.database import db -from monitoring.monitorlib.fetch import describe_flask_query +from monitoring.monitorlib.fetch import QueryType, describe_flask_query from monitoring.monitorlib.mutate.rid import UpdatedISA @@ -22,6 +23,7 @@ def rid_v19_operation(op_id: OperationID): @rid_v19_operation(OperationID.PostIdentificationServiceArea) +@query_type(QueryType.F3411v19USSPostIdentificationServiceArea) @requires_scope(Scope.Write) def riddp_notify_isa_v19(id: str): try: diff --git a/monitoring/mock_uss/riddp/routes_riddp_v22a.py b/monitoring/mock_uss/riddp/routes_riddp_v22a.py index 4a53dcb47a..d0058c8955 100644 --- a/monitoring/mock_uss/riddp/routes_riddp_v22a.py +++ b/monitoring/mock_uss/riddp/routes_riddp_v22a.py @@ -10,8 +10,9 @@ from monitoring.mock_uss.app import webapp from monitoring.mock_uss.auth import requires_scope +from monitoring.mock_uss.logging import query_type from monitoring.mock_uss.riddp.database import db -from monitoring.monitorlib.fetch import describe_flask_query +from monitoring.monitorlib.fetch import QueryType, describe_flask_query from monitoring.monitorlib.mutate.rid import UpdatedISA @@ -22,6 +23,7 @@ def rid_v22a_operation(op_id: OperationID): @rid_v22a_operation(OperationID.PostIdentificationServiceArea) +@query_type(QueryType.F3411v22aUSSPostIdentificationServiceArea) @requires_scope(Scope.ServiceProvider) def riddp_notify_isa_v22a(id: str): try: diff --git a/monitoring/mock_uss/ridsp/routes_ridsp_v19.py b/monitoring/mock_uss/ridsp/routes_ridsp_v19.py index f5e00553da..6ba34f08d2 100644 --- a/monitoring/mock_uss/ridsp/routes_ridsp_v19.py +++ b/monitoring/mock_uss/ridsp/routes_ridsp_v19.py @@ -26,7 +26,9 @@ from monitoring.mock_uss.app import webapp from monitoring.mock_uss.auth import requires_scope +from monitoring.mock_uss.logging import query_type from monitoring.monitorlib import geo +from monitoring.monitorlib.fetch import QueryType from monitoring.monitorlib.rid import RIDVersion from monitoring.monitorlib.rid_automated_testing.injection_api import TestFlight @@ -94,18 +96,8 @@ def rid_v19_operation(op_id: OperationID): return webapp.route("/mock/ridsp" + path, methods=[op.verb]) -@rid_v19_operation(OperationID.PostIdentificationServiceArea) -@requires_scope(Scope.Write) -def ridsp_notify_isa_v19(id: str): - return ( - flask.jsonify( - {"message": "mock_ridsp never solicits subscription notifications"} - ), - 400, - ) - - @rid_v19_operation(OperationID.SearchFlights) +@query_type(QueryType.F3411v19USSSearchFlights) @requires_scope(Scope.Read) def ridsp_flights_v19(): if "view" not in flask.request.args: @@ -150,6 +142,7 @@ def ridsp_flights_v19(): @rid_v19_operation(OperationID.GetFlightDetails) +@query_type(QueryType.F3411v19USSGetFlightDetails) @requires_scope(Scope.Read) def ridsp_flight_details_v19(id: str): now = arrow.utcnow().datetime diff --git a/monitoring/mock_uss/ridsp/routes_ridsp_v22a.py b/monitoring/mock_uss/ridsp/routes_ridsp_v22a.py index d4fcef50db..625c25d631 100644 --- a/monitoring/mock_uss/ridsp/routes_ridsp_v22a.py +++ b/monitoring/mock_uss/ridsp/routes_ridsp_v22a.py @@ -29,7 +29,9 @@ from monitoring.mock_uss.app import webapp from monitoring.mock_uss.auth import requires_scope +from monitoring.mock_uss.logging import query_type from monitoring.monitorlib import geo +from monitoring.monitorlib.fetch import QueryType from monitoring.monitorlib.rid import RIDVersion from monitoring.monitorlib.rid_automated_testing.injection_api import TestFlight from monitoring.monitorlib.rid_v2 import make_time @@ -151,18 +153,8 @@ def rid_v22a_operation(op_id: OperationID): return webapp.route("/mock/ridsp/v2" + path, methods=[op.verb]) -@rid_v22a_operation(OperationID.PostIdentificationServiceArea) -@requires_scope(Scope.ServiceProvider) -def ridsp_notify_isa_v22a(id: str): - return ( - flask.jsonify( - {"message": "mock_ridsp never solicits subscription notifications"} - ), - 400, - ) - - @rid_v22a_operation(OperationID.SearchFlights) +@query_type(QueryType.F3411v22aUSSSearchFlights) @requires_scope(Scope.DisplayProvider) def ridsp_flights_v22a(): if "view" not in flask.request.args: @@ -225,6 +217,7 @@ def ridsp_flights_v22a(): @rid_v22a_operation(OperationID.GetFlightDetails) +@query_type(QueryType.F3411v22aUSSGetFlightDetails) @requires_scope(Scope.DisplayProvider) def ridsp_flight_details_v22a(id: str): now = arrow.utcnow().datetime From 1481573a363294edf71b6e8fc6d31ebf25726cab Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Wed, 13 May 2026 21:01:46 +0000 Subject: [PATCH 2/4] Improve participant identification from notifications --- .basedpyright/baseline.json | 848 +++--------------- .../dev/library/environment_containers.yaml | 6 +- .../dev/library/environment_localhost.yaml | 4 +- .../resources/interuss/uss_identification.py | 86 +- .../astm/netrid/common/dp_behavior.py | 2 +- .../netrid/common/sp_notification_behavior.py | 143 ++- .../constraint_ref_synchronization.py | 2 + .../op_intent_ref_synchronization.py | 3 +- .../scenarios/interuss/mock_uss/test_steps.py | 15 +- .../uss_qualifier/scenarios/scenario.py | 25 +- .../suites/astm/netrid/f3411_22a.yaml | 1 + .../uss_identification/USSIdentifiers.json | 4 +- 12 files changed, 300 insertions(+), 839 deletions(-) diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index 1b070c6e24..71e4a247ef 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -7773,14 +7773,6 @@ "lineCount": 1 } }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 46, - "endColumn": 64, - "lineCount": 1 - } - }, { "code": "reportOptionalMemberAccess", "range": { @@ -7805,14 +7797,6 @@ "lineCount": 1 } }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 16, - "endColumn": 30, - "lineCount": 1 - } - }, { "code": "reportArgumentType", "range": { @@ -7829,14 +7813,6 @@ "lineCount": 1 } }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 21, - "endColumn": 49, - "lineCount": 1 - } - }, { "code": "reportOptionalMemberAccess", "range": { @@ -7844,22 +7820,6 @@ "endColumn": 72, "lineCount": 1 } - }, - { - "code": "reportInvalidTypeForm", - "range": { - "startColumn": 26, - "endColumn": 42, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 16, - "endColumn": 46, - "lineCount": 1 - } } ], "./monitoring/uss_qualifier/scenarios/astm/netrid/common/sp_operator_notify_missing_fields.py": [ @@ -16116,786 +16076,338 @@ { "code": "reportArgumentType", "range": { - "startColumn": 42, - "endColumn": 52, + "startColumn": 16, + "endColumn": 42, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 42, - "endColumn": 52, + "startColumn": 63, + "endColumn": 71, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 42, - "endColumn": 52, + "startColumn": 67, + "endColumn": 75, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 42, - "endColumn": 52, + "startColumn": 54, + "endColumn": 61, "lineCount": 1 } }, { "code": "reportArgumentType", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 33, + "endColumn": 53, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 50, + "endColumn": 53, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 54, + "endColumn": 61, "lineCount": 1 } }, { "code": "reportArgumentType", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 33, + "endColumn": 53, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 42, - "endColumn": 52, + "startColumn": 50, + "endColumn": 53, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 42, - "endColumn": 52, + "startColumn": 41, + "endColumn": 44, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportPossiblyUnboundVariable", "range": { - "startColumn": 42, - "endColumn": 52, + "startColumn": 27, + "endColumn": 28, "lineCount": 1 } }, { "code": "reportArgumentType", "range": { - "startColumn": 42, - "endColumn": 52, + "startColumn": 29, + "endColumn": 49, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 46, + "endColumn": 49, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 47, + "startColumn": 50, "endColumn": 57, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportPossiblyUnboundVariable", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 27, + "endColumn": 29, "lineCount": 1 } }, { "code": "reportArgumentType", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 33, + "endColumn": 53, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 16, - "endColumn": 42, + "startColumn": 50, + "endColumn": 53, "lineCount": 1 } }, { - "code": "reportOptionalMemberAccess", + "code": "reportPossiblyUnboundVariable", "range": { - "startColumn": 63, - "endColumn": 71, + "startColumn": 27, + "endColumn": 28, "lineCount": 1 } }, { - "code": "reportOptionalMemberAccess", + "code": "reportArgumentType", "range": { - "startColumn": 67, - "endColumn": 75, + "startColumn": 29, + "endColumn": 49, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 42, - "endColumn": 52, + "startColumn": 46, + "endColumn": 49, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 42, - "endColumn": 52, + "startColumn": 50, + "endColumn": 57, "lineCount": 1 } }, { "code": "reportArgumentType", "range": { - "startColumn": 42, - "endColumn": 52, + "startColumn": 34, + "endColumn": 35, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 42, - "endColumn": 52, + "startColumn": 21, + "endColumn": 32, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 17, + "endColumn": 28, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 56, + "endColumn": 67, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 40, + "endColumn": 47, "lineCount": 1 } }, { "code": "reportArgumentType", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 59, + "endColumn": 87, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportPossiblyUnboundVariable", "range": { - "startColumn": 42, - "endColumn": 52, + "startColumn": 50, + "endColumn": 53, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportOptionalMemberAccess", "range": { - "startColumn": 42, - "endColumn": 52, + "startColumn": 40, + "endColumn": 47, "lineCount": 1 } - }, + } + ], + "./monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/op_intent_ref_synchronization.py": [ { "code": "reportArgumentType", "range": { - "startColumn": 42, - "endColumn": 52, + "startColumn": 37, + "endColumn": 51, "lineCount": 1 } }, { "code": "reportArgumentType", "range": { - "startColumn": 42, - "endColumn": 52, + "startColumn": 33, + "endColumn": 44, "lineCount": 1 } }, { "code": "reportArgumentType", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 24, + "endColumn": 43, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportPossiblyUnboundVariable", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 19, + "endColumn": 20, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportPossiblyUnboundVariable", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 97, + "endColumn": 98, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportPossiblyUnboundVariable", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 42, + "endColumn": 43, "lineCount": 1 } }, { - "code": "reportOptionalMemberAccess", + "code": "reportPossiblyUnboundVariable", "range": { - "startColumn": 54, - "endColumn": 61, + "startColumn": 20, + "endColumn": 23, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportPossiblyUnboundVariable", "range": { - "startColumn": 33, - "endColumn": 53, + "startColumn": 18, + "endColumn": 19, "lineCount": 1 } }, { - "code": "reportOptionalMemberAccess", + "code": "reportArgumentType", "range": { - "startColumn": 50, - "endColumn": 53, + "startColumn": 59, + "endColumn": 87, "lineCount": 1 } }, { - "code": "reportOptionalMemberAccess", + "code": "reportPossiblyUnboundVariable", "range": { - "startColumn": 54, - "endColumn": 61, + "startColumn": 28, + "endColumn": 32, "lineCount": 1 } }, { - "code": "reportArgumentType", - "range": { - "startColumn": 33, - "endColumn": 53, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 50, - "endColumn": 53, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 41, - "endColumn": 44, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 27, - "endColumn": 28, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 29, - "endColumn": 49, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 46, - "endColumn": 49, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 50, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 27, - "endColumn": 29, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 33, - "endColumn": 53, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 50, - "endColumn": 53, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 27, - "endColumn": 28, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 29, - "endColumn": 49, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 46, - "endColumn": 49, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 50, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 34, - "endColumn": 35, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 21, - "endColumn": 32, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 17, - "endColumn": 28, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 56, - "endColumn": 67, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 40, - "endColumn": 47, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 59, - "endColumn": 87, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 50, - "endColumn": 53, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 40, - "endColumn": 47, - "lineCount": 1 - } - } - ], - "./monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/op_intent_ref_synchronization.py": [ - { - "code": "reportArgumentType", - "range": { - "startColumn": 37, - "endColumn": 51, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 33, - "endColumn": 44, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 24, - "endColumn": 43, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 19, - "endColumn": 20, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 97, - "endColumn": 98, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 42, - "endColumn": 43, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 20, - "endColumn": 23, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 18, - "endColumn": 19, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 59, - "endColumn": 87, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 28, - "endColumn": 32, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 42, - "endColumn": 43, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 20, - "endColumn": 23, - "lineCount": 1 - } - }, - { - "code": "reportPossiblyUnboundVariable", - "range": { - "startColumn": 18, - "endColumn": 19, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", + "code": "reportPossiblyUnboundVariable", "range": { "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, + "endColumn": 43, "lineCount": 1 } }, { "code": "reportArgumentType", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 20, + "endColumn": 23, "lineCount": 1 } }, { - "code": "reportArgumentType", + "code": "reportPossiblyUnboundVariable", "range": { - "startColumn": 47, - "endColumn": 57, + "startColumn": 18, + "endColumn": 19, "lineCount": 1 } }, @@ -16923,134 +16435,6 @@ "lineCount": 1 } }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 42, - "endColumn": 52, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 47, - "endColumn": 57, - "lineCount": 1 - } - }, { "code": "reportOptionalOperand", "range": { diff --git a/monitoring/uss_qualifier/configurations/dev/library/environment_containers.yaml b/monitoring/uss_qualifier/configurations/dev/library/environment_containers.yaml index d99bbcb946..d2f5b4015e 100644 --- a/monitoring/uss_qualifier/configurations/dev/library/environment_containers.yaml +++ b/monitoring/uss_qualifier/configurations/dev/library/environment_containers.yaml @@ -312,11 +312,11 @@ uss_identification: specification: uss_identifiers: uss1: - astm_url_regexes: + server_url_regexes: - 'http://[^/]*uss1\.localutm.*' uss2: - astm_url_regexes: + server_url_regexes: - 'http://[^/]*uss2\.localutm.*' uss3: - astm_url_regexes: + server_url_regexes: - 'http://[^/]*uss3\.localutm.*' diff --git a/monitoring/uss_qualifier/configurations/dev/library/environment_localhost.yaml b/monitoring/uss_qualifier/configurations/dev/library/environment_localhost.yaml index e8da109249..1820cf11f3 100644 --- a/monitoring/uss_qualifier/configurations/dev/library/environment_localhost.yaml +++ b/monitoring/uss_qualifier/configurations/dev/library/environment_localhost.yaml @@ -279,8 +279,8 @@ uss_identification: specification: uss_identifiers: uss1: - astm_url_regexes: + server_url_regexes: - 'http://[^/]*localhost:8074.*' uss2: - astm_url_regexes: + server_url_regexes: - 'http://[^/]*localhost:8094.*' diff --git a/monitoring/uss_qualifier/resources/interuss/uss_identification.py b/monitoring/uss_qualifier/resources/interuss/uss_identification.py index b6958280e7..c35fa1110c 100644 --- a/monitoring/uss_qualifier/resources/interuss/uss_identification.py +++ b/monitoring/uss_qualifier/resources/interuss/uss_identification.py @@ -1,8 +1,9 @@ import re from implicitdict import ImplicitDict, Optional +from loguru import logger -from monitoring.monitorlib.fetch import Query +from monitoring.monitorlib.fetch import Query, QueryType from monitoring.uss_qualifier.configurations.configuration import ParticipantID from monitoring.uss_qualifier.resources.resource import Resource @@ -16,12 +17,19 @@ class AccessTokenIdentifier(ImplicitDict): class USSIdentifiers(ImplicitDict): - astm_url_regexes: Optional[list[str]] - """If a URL to an ASTM (F3411, F3548, etc) endpoint matches one of these regular expressions, assume the participant is responsible for that server""" + server_url_regexes: Optional[list[str]] + """If a URL to an endpoint matches one of these regular expressions, assume the participant is responsible for that server""" access_tokens: Optional[list[AccessTokenIdentifier]] """If an access token matches one of these identifiers, assume the participant is responsible for that access token""" + def matches_server_url(self, url: str) -> bool: + if "server_url_regexes" in self and self.server_url_regexes: + for url_regex in self.server_url_regexes: + if re.fullmatch(url_regex, url): + return True + return False + class USSIdentificationSpecification(ImplicitDict): uss_identifiers: dict[ParticipantID, USSIdentifiers] @@ -40,7 +48,8 @@ def __init__( super().__init__(specification, resource_origin) self.identifiers = specification.uss_identifiers or {} - def attribute_query(self, query: Query) -> None: + def attribute_query_server(self, query: Query) -> None: + """Identify the participant ID of the server responding to this query and mutate `query` accordingly, if possible""" claims = query.request.token if "error" in claims and len(claims) == 1: claims = None @@ -48,10 +57,58 @@ def attribute_query(self, query: Query) -> None: for participant_id, identifiers in self.identifiers.items(): attribute_to_participant = False - if "astm_url_regexes" in identifiers and identifiers.astm_url_regexes: - for url_regex in identifiers.astm_url_regexes: - if re.fullmatch(url_regex, query.request.url): - attribute_to_participant = True + if "server_url_regexes" in identifiers and identifiers.server_url_regexes: + # The participant is responsible for the server end of the query when the query URL matches one of the participant's + if identifiers.matches_server_url(query.request.url): + attribute_to_participant = True + + if attribute_to_participant: + query.participant_id = participant_id + + def identify_query_client(self, query: Query) -> ParticipantID | None: + """Identify the participant ID of the client making this query if possible""" + claims = query.request.token + if "error" in claims and len(claims) == 1: + claims = None + + query_type = query.query_type if "query_type" in query else None + if query_type == QueryType.F3411v22aUSSPostIdentificationServiceArea: + url = ( + (query.request.json or {}) + .get("service_area", {}) + .get("uss_base_url", None) + ) + elif query_type == QueryType.F3411v19USSPostIdentificationServiceArea: + url = ( + (query.request.json or {}) + .get("service_area", {}) + .get("flights_url", None) + ) + elif query_type == QueryType.F3548v21USSNotifyOperationalIntentDetailsChanged: + url = ( + (query.request.json or {}) + .get("operational_intent", {}) + .get("reference", {}) + .get("uss_base_url", None) + ) + elif query_type == QueryType.F3548v21USSNotifyConstraintDetailsChanged: + url = ( + (query.request.json or {}) + .get("constraint", {}) + .get("reference", {}) + .get("uss_base_url", None) + ) + else: + url = None + + matching_participants = [] + for participant_id, identifiers in self.identifiers.items(): + attribute_to_participant = False + + if "server_url_regexes" in identifiers and identifiers.server_url_regexes: + # The participant is responsible for the client end of the query when the request body is a payload specifying a callback server operated by the participant + if url and identifiers.matches_server_url(url): + attribute_to_participant = True if "access_tokens" in identifiers and identifiers.access_tokens: for access_token_identifier in identifiers.access_tokens: @@ -70,4 +127,15 @@ def attribute_query(self, query: Query) -> None: attribute_to_participant = True if attribute_to_participant: - query.participant_id = participant_id + matching_participants.append(participant_id) + + if len(matching_participants) == 1: + return matching_participants[0] + elif len(matching_participants) > 1: + logger.warning( + "Multiple participants {} match as clients for query {} {}", + ", ".join(matching_participants), + query.request.method, + query.request.url, + ) + return None diff --git a/monitoring/uss_qualifier/scenarios/astm/netrid/common/dp_behavior.py b/monitoring/uss_qualifier/scenarios/astm/netrid/common/dp_behavior.py index 139dbb4485..032b1151c1 100644 --- a/monitoring/uss_qualifier/scenarios/astm/netrid/common/dp_behavior.py +++ b/monitoring/uss_qualifier/scenarios/astm/netrid/common/dp_behavior.py @@ -182,7 +182,7 @@ def _step_create_isa(self): if self._identification is not None: # Attribute notifications to participants when possible for base_url, notification in isa_change.notifications.items(): - self._identification.attribute_query(notification.query) + self._identification.attribute_query_server(notification.query) # For any attributed notifications, check that the recipient acknowledged them correctly for base_url, notification in isa_change.notifications.items(): diff --git a/monitoring/uss_qualifier/scenarios/astm/netrid/common/sp_notification_behavior.py b/monitoring/uss_qualifier/scenarios/astm/netrid/common/sp_notification_behavior.py index 2fb117675d..d035b30bc8 100644 --- a/monitoring/uss_qualifier/scenarios/astm/netrid/common/sp_notification_behavior.py +++ b/monitoring/uss_qualifier/scenarios/astm/netrid/common/sp_notification_behavior.py @@ -1,28 +1,28 @@ from collections.abc import Callable -from datetime import timedelta +from datetime import datetime, timedelta from typing import TypeVar -from future.backports.datetime import datetime -from implicitdict import ImplicitDict -from requests.exceptions import RequestException +import arrow from s2sphere import LatLngRect -from uas_standards.astm.f3411.v19 import api as api_v19 -from uas_standards.astm.f3411.v22a import api as api_v22a from monitoring.monitorlib.clients.mock_uss.interactions import ( Interaction, QueryDirection, ) -from monitoring.monitorlib.errors import stacktrace_string +from monitoring.monitorlib.fetch import Query, QueryType from monitoring.monitorlib.rid import RIDVersion from monitoring.monitorlib.temporal import Time from monitoring.prober.infrastructure import register_resource_type +from monitoring.uss_qualifier.configurations.configuration import ParticipantID from monitoring.uss_qualifier.resources.astm.f3411.dss import DSSInstancesResource from monitoring.uss_qualifier.resources.interuss import IDGeneratorResource from monitoring.uss_qualifier.resources.interuss.mock_uss.client import ( MockUSSClient, MockUSSResource, ) +from monitoring.uss_qualifier.resources.interuss.uss_identification import ( + USSIdentificationResource, +) from monitoring.uss_qualifier.resources.netrid import ( FlightDataResource, NetRIDServiceProviders, @@ -36,7 +36,7 @@ from monitoring.uss_qualifier.scenarios.interuss.mock_uss.test_steps import ( direction_filter, get_mock_uss_interactions, - status_code_filter, + query_type_filter, ) from monitoring.uss_qualifier.scenarios.scenario import GenericTestScenario from monitoring.uss_qualifier.suites.suite import ExecutionContext @@ -50,6 +50,7 @@ class ServiceProviderNotificationBehavior(GenericTestScenario): _flights_data: FlightDataResource _service_providers: NetRIDServiceProviders _mock_uss: MockUSSClient + _uss_identification: USSIdentificationResource _injected_flights: list[InjectedFlight] _injected_tests: list[InjectedTest] @@ -64,11 +65,13 @@ def __init__( mock_uss: MockUSSResource, id_generator: IDGeneratorResource, dss_pool: DSSInstancesResource, + uss_identification: USSIdentificationResource, ): super().__init__() self._flights_data = flights_data self._service_providers = service_providers self._mock_uss = mock_uss.mock_uss + self._uss_identification = uss_identification self._dss_wrapper = DSSWrapper(self, dss_pool.dss_instances[0]) self._injected_tests = [] @@ -98,14 +101,12 @@ def run(self, context: ExecutionContext): self.end_test_step() self.begin_test_step("Injection") + injection_time = arrow.utcnow().datetime self._inject_flights() self.end_test_step() self.begin_test_step("Validate Mock USS received notification") - # Given that we know when flights are injected, we could narrow down the time window for - # which we are looking for notifications to something more precise than scenario start time. - # TODO tracked in #1052 - self._validate_mock_uss_notifications(context.start_time) + self._validate_mock_uss_notifications(injection_time) self.end_test_step() self.end_test_case() @@ -154,42 +155,56 @@ def _validate_mock_uss_notifications(self, notifications_received_after: datetim [tf.uss_participant_id for tf in self._injected_flights] ) - def post_isa_filter(interaction: Interaction): - return ( - interaction.query.request.method == "POST" - and "/uss/identification_service_areas/" - in interaction.query.request.url - ) - - def fetch_interactions() -> list[Interaction]: + def due_to_subscription_filter(interaction: Interaction) -> bool: + """Select only interactions that are notifications due to subscription we established""" + subscriptions = [ + sub.get("subscription_id", None) + for sub in (interaction.query.request.json or {}).get( + "subscriptions", [] + ) + ] + if self._subscription_id not in subscriptions: + return False + return True + + def fetch_interactions() -> tuple[list[Interaction], Query]: + if self._rid_version == RIDVersion.f3411_22a: + query_type = QueryType.F3411v22aUSSPostIdentificationServiceArea + elif self._rid_version == RIDVersion.f3411_19: + query_type = QueryType.F3411v19USSPostIdentificationServiceArea + else: + raise NotImplementedError() return get_mock_uss_interactions( self, self._mock_uss, Time(notifications_received_after), direction_filter(QueryDirection.Incoming), - status_code_filter(204), - post_isa_filter, - )[0] + query_type_filter(query_type), + due_to_subscription_filter, + ) - def includes_all_notifications(raw_interactions: list[Interaction]) -> bool: + def includes_all_notifications( + raw_interactions: tuple[list[Interaction], Query], + ) -> bool: pids_having_notified = self._relevant_notified_subs( - raw_interactions, relevant_participant_ids, notifications_received_after + raw_interactions[0], relevant_participant_ids ) # We're done once we have a notification from each SP we injected a flight in return len(pids_having_notified) == len(relevant_participant_ids) # notifications are not immediate: we optimistically try early, and retry until # the permissible delay has passed, or we have received all notifications. - interactions = self._retry_with_backoff( + interactions, query = self._retry_with_backoff( fetch_interactions, retries=3, delay_s=1, delay_reason="waiting for expected notifications to be delivered", was_successful=includes_all_notifications, ) + # fish out the notification times per participant ID notifs_by_participant = self._relevant_notified_subs( - interactions, relevant_participant_ids, notifications_received_after + interactions, relevant_participant_ids ) # For each of the service providers we injected flights in, # check that we received a notification. We don't check the latency as a single datapoint does not allow @@ -208,61 +223,27 @@ def includes_all_notifications(raw_interactions: list[Interaction]) -> bool: check.record_failed( summary="No notification received", details=f"No notification received within roughly {self._rid_version.dp_data_resp_percentile99_s} seconds from {test_flight.uss_participant_id} for subscription {self._subscription_id} about flight {test_flight.test_id} that happened within the subscription's boundaries.", + queries=query, ) continue - def _notif_operation_id(self) -> api_v19.OperationID | api_v22a.OperationID: - if self._rid_version.f3411_19: - return api_v19.OperationID.PostIdentificationServiceArea - elif self._rid_version.f3411_22a: - return api_v22a.OperationID.PostIdentificationServiceArea - else: - raise ValueError(f"Unsupported RID version: {self._rid_version}") - - def _notif_param_type( - self, - ) -> type[ - api_v19.PutIdentificationServiceAreaNotificationParameters - | api_v22a.PutIdentificationServiceAreaNotificationParameters - ]: - if self._rid_version == RIDVersion.f3411_19: - return api_v19.PutIdentificationServiceAreaNotificationParameters - elif self._rid_version == RIDVersion.f3411_22a: - return api_v22a.PutIdentificationServiceAreaNotificationParameters - else: - raise ValueError(f"Unsupported RID version: {self._rid_version}") - def _relevant_notified_subs( self, raw_interactions: list[Interaction], relevant_pids: set[str], - received_after: datetime, ) -> dict[str, list[datetime]]: - # Parse version-specific notification parameters - PutIsaParamsType = self._notif_param_type() - - relevant = [] + notifs_by_participant: dict[ParticipantID, list[datetime]] = {} for interaction in raw_interactions: - received_at = interaction.query.request.received_at.datetime - notification: PutIsaParamsType = ImplicitDict.parse( - interaction.query.request.json, PutIsaParamsType + # Associate queries' clients to participants where possible + participant_id = self._uss_identification.identify_query_client( + interaction.query ) - for sub in notification.subscriptions: - if ( - sub.subscription_id == self._subscription_id - and "service_area" - in notification # deletion notification don't have this field - and notification.service_area.owner in relevant_pids - # We may sometimes receive slightly older and unrelated notifications which we filter out - and received_at > received_after - ): - relevant.append((received_at, notification.service_area.owner)) - - notifs_by_participant: dict[str, list[datetime]] = {} - for received_at, participant_id in relevant: - if participant_id not in notifs_by_participant: - notifs_by_participant[participant_id] = [] - notifs_by_participant[participant_id].append(received_at) + if not participant_id or participant_id not in relevant_pids: + continue + notifs = notifs_by_participant.get(participant_id, []) + notifs.append(interaction.query.request.received_at.datetime) + notifs_by_participant[participant_id] = notifs + return notifs_by_participant def cleanup(self): @@ -285,19 +266,15 @@ def cleanup(self): ) sp = matching_sps[0] check = self.check("Successful test deletion", [sp.participant_id]) - try: - query = sp.delete_test(injected_test.test_id, injected_test.version) - self.record_query(query) - if query.status_code != 200: - raise ValueError( - f"Received status code {query.status_code} after attempting to delete test {injected_test.test_id} at version {injected_test.version} from service provider {sp.participant_id}" - ) + query = sp.delete_test(injected_test.test_id, injected_test.version) + self.record_query(query) + if query.status_code == 200: check.record_passed() - except (RequestException, ValueError) as e: - stacktrace = stacktrace_string(e) + else: check.record_failed( - summary="Error while trying to delete test flight", - details=f"While trying to delete a test flight from {sp.participant_id}, encountered error:\n{stacktrace}", + summary="Test flight deletion was unsuccessful", + details=f"Received status code {query.status_code} after attempting to delete test {injected_test.test_id} at version {injected_test.version} from service provider {sp.participant_id}", + queries=query, ) self.end_cleanup() diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/constraint_ref_synchronization.py b/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/constraint_ref_synchronization.py index 9ae8f32650..120e58ce5a 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/constraint_ref_synchronization.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/constraint_ref_synchronization.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta +from typing import Any from uas_standards.astm.f3548.v21.api import ( ConstraintReference, @@ -343,6 +344,7 @@ def _validate_cr_from_secondary( involved_participants: list[str], from_search: bool = False, ): + check_args: dict[str, Any] = {} with self.check(main_check_name, involved_participants) as main_check: with self.check( "Propagated constraint reference contains the correct manager", diff --git a/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/op_intent_ref_synchronization.py b/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/op_intent_ref_synchronization.py index f9f20703ed..7232a12920 100644 --- a/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/op_intent_ref_synchronization.py +++ b/monitoring/uss_qualifier/scenarios/astm/utm/dss/synchronization/op_intent_ref_synchronization.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta +from typing import Any from uas_standards.astm.f3548.v21.api import ( EntityID, @@ -351,7 +352,7 @@ def _validate_oir_from_secondary( involved_participants: list[str], ): # TODO: this main check mechanism may be removed if we are able to specify requirements to be validated in test step fragments - + check_args: dict[str, Any] = {} with self.check(main_check_name, involved_participants) as main_check: with self.check( "Propagated operational intent reference contains the correct manager", diff --git a/monitoring/uss_qualifier/scenarios/interuss/mock_uss/test_steps.py b/monitoring/uss_qualifier/scenarios/interuss/mock_uss/test_steps.py index b679fa4e77..39aede2757 100644 --- a/monitoring/uss_qualifier/scenarios/interuss/mock_uss/test_steps.py +++ b/monitoring/uss_qualifier/scenarios/interuss/mock_uss/test_steps.py @@ -9,7 +9,7 @@ Interaction, QueryDirection, ) -from monitoring.monitorlib.fetch import Query, QueryError +from monitoring.monitorlib.fetch import Query, QueryError, QueryType from monitoring.uss_qualifier.resources.interuss.mock_uss.client import MockUSSClient from monitoring.uss_qualifier.scenarios.scenario import TestScenarioType @@ -132,3 +132,16 @@ def is_applicable(interaction: Interaction) -> bool: return interaction.query.status_code == status_code return is_applicable + + +def query_type_filter(*query_type: QueryType | None) -> Callable[[Interaction], bool]: + def is_applicable(interaction: Interaction) -> bool: + if "query_type" in interaction.query and interaction.query.query_type: + if interaction.query.query_type in query_type: + return True + else: + if None in query_type: + return True + return False + + return is_applicable diff --git a/monitoring/uss_qualifier/scenarios/scenario.py b/monitoring/uss_qualifier/scenarios/scenario.py index 2d121282ee..683e0752b8 100644 --- a/monitoring/uss_qualifier/scenarios/scenario.py +++ b/monitoring/uss_qualifier/scenarios/scenario.py @@ -14,7 +14,7 @@ from monitoring import uss_qualifier as uss_qualifier_module from monitoring.monitorlib import fetch, inspection from monitoring.monitorlib.errors import current_stack_string -from monitoring.monitorlib.fetch import QueryType +from monitoring.monitorlib.fetch import Query, QueryType from monitoring.monitorlib.inspection import fullname from monitoring.monitorlib.temporal import TestTimeContext from monitoring.uss_qualifier import scenarios as scenarios_module @@ -127,6 +127,7 @@ def record_failed( details: str = "", query_timestamps: list[datetime] | None = None, additional_data: dict | None = None, + queries: Query | Iterable[Query] | None = None, ) -> None: self._outcome_recorded = True if "severity" in self._documentation and self._documentation.severity: @@ -159,10 +160,24 @@ def record_failed( } if additional_data is not None: kwargs["additional_data"] = additional_data - if query_timestamps is not None: - kwargs["query_report_timestamps"] = [ - StringBasedDateTime(t) for t in query_timestamps - ] + if query_timestamps is not None or queries is not None: + query_report_timestamps = [] + if query_timestamps is not None: + query_report_timestamps.extend( + StringBasedDateTime(t) for t in query_timestamps + ) + if isinstance(queries, Query): + if "initiated_at" in queries.request and queries.request.initiated_at: + query_report_timestamps.append( + StringBasedDateTime(queries.request.initiated_at) + ) + elif queries: + for query in queries: + if "initiated_at" in query.request and query.request.initiated_at: + query_report_timestamps.append( + StringBasedDateTime(query.request.initiated_at) + ) + kwargs["query_report_timestamps"] = query_report_timestamps failed_check = FailedCheck(**kwargs) self._step_report.failed_checks.append(failed_check) if self._on_failed_check is not None: diff --git a/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.yaml b/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.yaml index f0032054a8..7aeee5a536 100644 --- a/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.yaml +++ b/monitoring/uss_qualifier/suites/astm/netrid/f3411_22a.yaml @@ -94,6 +94,7 @@ actions: mock_uss: mock_uss_dp id_generator: id_generator dss_pool: dss_instances + uss_identification: uss_identification on_failure: Continue - test_scenario: scenario_type: scenarios.astm.netrid.v22a.NominalBehavior diff --git a/schemas/monitoring/uss_qualifier/resources/interuss/uss_identification/USSIdentifiers.json b/schemas/monitoring/uss_qualifier/resources/interuss/uss_identification/USSIdentifiers.json index 2be17a27ec..36c3d4b25f 100644 --- a/schemas/monitoring/uss_qualifier/resources/interuss/uss_identification/USSIdentifiers.json +++ b/schemas/monitoring/uss_qualifier/resources/interuss/uss_identification/USSIdentifiers.json @@ -17,8 +17,8 @@ "null" ] }, - "astm_url_regexes": { - "description": "If a URL to an ASTM (F3411, F3548, etc) endpoint matches one of these regular expressions, assume the participant is responsible for that server", + "server_url_regexes": { + "description": "If a URL to an endpoint matches one of these regular expressions, assume the participant is responsible for that server", "items": { "type": "string" }, From f148f12bc3cd7df6b34b4cb0a346d9f4808917ae Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Wed, 13 May 2026 22:41:41 +0000 Subject: [PATCH 3/4] Add missing resource documentation and missing F3411-19 resource --- .../scenarios/astm/netrid/v19/sp_notification_behavior.md | 4 ++++ .../scenarios/astm/netrid/v22a/sp_notification_behavior.md | 4 ++++ monitoring/uss_qualifier/suites/astm/netrid/f3411_19.yaml | 1 + 3 files changed, 9 insertions(+) diff --git a/monitoring/uss_qualifier/scenarios/astm/netrid/v19/sp_notification_behavior.md b/monitoring/uss_qualifier/scenarios/astm/netrid/v19/sp_notification_behavior.md index 0705adda54..50e66a4126 100644 --- a/monitoring/uss_qualifier/scenarios/astm/netrid/v19/sp_notification_behavior.md +++ b/monitoring/uss_qualifier/scenarios/astm/netrid/v19/sp_notification_behavior.md @@ -28,6 +28,10 @@ A [`DSSInstancesResource`](../../../../resources/astm/f3411/dss.py) from which a [`IDGeneratorResource`](../../../../resources/interuss/id_generator.py) providing the Subscription ID for this scenario. +### uss_identification + +[`USSIdentificationResource`](../../../../resources/interuss/uss_identification.py) describing how to identify participants responsible for observed notifications. + ## Setup test case ### [Clean workspace test step](./dss/test_steps/clean_workspace.md) diff --git a/monitoring/uss_qualifier/scenarios/astm/netrid/v22a/sp_notification_behavior.md b/monitoring/uss_qualifier/scenarios/astm/netrid/v22a/sp_notification_behavior.md index 8646eead8d..9721612172 100644 --- a/monitoring/uss_qualifier/scenarios/astm/netrid/v22a/sp_notification_behavior.md +++ b/monitoring/uss_qualifier/scenarios/astm/netrid/v22a/sp_notification_behavior.md @@ -28,6 +28,10 @@ A [`DSSInstancesResource`](../../../../resources/astm/f3411/dss.py) from which a [`IDGeneratorResource`](../../../../resources/interuss/id_generator.py) providing the Subscription ID for this scenario. +### uss_identification + +[`USSIdentificationResource`](../../../../resources/interuss/uss_identification.py) describing how to identify participants responsible for observed notifications. + ## Setup test case ### [Clean workspace test step](./dss/test_steps/clean_workspace.md) diff --git a/monitoring/uss_qualifier/suites/astm/netrid/f3411_19.yaml b/monitoring/uss_qualifier/suites/astm/netrid/f3411_19.yaml index 2312792d69..dd9a0bfa88 100644 --- a/monitoring/uss_qualifier/suites/astm/netrid/f3411_19.yaml +++ b/monitoring/uss_qualifier/suites/astm/netrid/f3411_19.yaml @@ -85,6 +85,7 @@ actions: mock_uss: mock_uss_dp id_generator: id_generator dss_pool: dss_instances + uss_identification: uss_identification? on_failure: Continue - test_scenario: scenario_type: scenarios.astm.netrid.v19.NominalBehavior From 68c7fce85a944ac8635b7b427b155a70eb0f7bd7 Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Thu, 14 May 2026 16:45:34 +0000 Subject: [PATCH 4/4] Address comments --- NEXT_RELEASE_NOTES.md | 2 ++ monitoring/uss_qualifier/suites/astm/netrid/f3411_19.yaml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/NEXT_RELEASE_NOTES.md b/NEXT_RELEASE_NOTES.md index c5df1b32bc..9951f6b0fc 100644 --- a/NEXT_RELEASE_NOTES.md +++ b/NEXT_RELEASE_NOTES.md @@ -37,6 +37,8 @@ The release notes should contain at least the following sections: ## Mandatory migration tasks +The field `astm_url_regexes` in the USSIdentificationResource has been changed to `server_url_regexes`. Any test configurations using this resource (likely NetRID configurations) must change `astm_url_regexes` to `server_url_regexes`. + ## Optional migration tasks ## Important information diff --git a/monitoring/uss_qualifier/suites/astm/netrid/f3411_19.yaml b/monitoring/uss_qualifier/suites/astm/netrid/f3411_19.yaml index dd9a0bfa88..94f0f16277 100644 --- a/monitoring/uss_qualifier/suites/astm/netrid/f3411_19.yaml +++ b/monitoring/uss_qualifier/suites/astm/netrid/f3411_19.yaml @@ -85,7 +85,7 @@ actions: mock_uss: mock_uss_dp id_generator: id_generator dss_pool: dss_instances - uss_identification: uss_identification? + uss_identification: uss_identification on_failure: Continue - test_scenario: scenario_type: scenarios.astm.netrid.v19.NominalBehavior