From 6cd9954ca468712742440c54fcc3712bc4649d67 Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Fri, 20 Feb 2026 01:11:13 +0000 Subject: [PATCH] Make op_intent optional in mock_uss FlightRecord --- .basedpyright/baseline.json | 40 ------ .../mock_uss/f3548v21/flight_planning.py | 119 +++++++++++------- monitoring/mock_uss/f3548v21/routes_scd.py | 8 +- monitoring/mock_uss/flights/database.py | 5 +- .../scd_injection/routes_injection.py | 79 ++++++------ 5 files changed, 117 insertions(+), 134 deletions(-) diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index e496585129..1c69fa4d96 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -1163,14 +1163,6 @@ "lineCount": 1 } }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 32, - "endColumn": 41, - "lineCount": 1 - } - }, { "code": "reportOptionalMemberAccess", "range": { @@ -1203,22 +1195,6 @@ "lineCount": 1 } }, - { - "code": "reportOperatorIssue", - "range": { - "startColumn": 12, - "endColumn": 62, - "lineCount": 2 - } - }, - { - "code": "reportArgumentType", - "range": { - "startColumn": 12, - "endColumn": 62, - "lineCount": 2 - } - }, { "code": "reportOperatorIssue", "range": { @@ -1855,22 +1831,6 @@ "lineCount": 1 } }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 49, - "endColumn": 55, - "lineCount": 1 - } - }, - { - "code": "reportOptionalMemberAccess", - "range": { - "startColumn": 68, - "endColumn": 71, - "lineCount": 1 - } - }, { "code": "reportOptionalMemberAccess", "range": { diff --git a/monitoring/mock_uss/f3548v21/flight_planning.py b/monitoring/mock_uss/f3548v21/flight_planning.py index 4d5ce937d1..451e0fc89b 100644 --- a/monitoring/mock_uss/f3548v21/flight_planning.py +++ b/monitoring/mock_uss/f3548v21/flight_planning.py @@ -11,7 +11,7 @@ from monitoring.mock_uss.app import webapp from monitoring.mock_uss.config import KEY_BASE_URL from monitoring.mock_uss.f3548v21 import utm_client -from monitoring.mock_uss.flights.database import FlightRecord, db +from monitoring.mock_uss.flights.database import Database, FlightRecord, db from monitoring.monitorlib.clients import scd as scd_client from monitoring.monitorlib.clients.flight_planning.flight_info import FlightInfo from monitoring.monitorlib.fetch import QueryError @@ -104,9 +104,10 @@ def conflicts_with_flightrecords( ) for other_flight in flights: - if not other_flight: + if not other_flight or not other_flight.op_intent: continue + # TODO(mock_uss_flight_id): Use flight ID that is independent of op_intent if other_flight.op_intent.reference.id == op_intent.reference.id: # Same flight continue @@ -158,6 +159,7 @@ def log(msg): for op_intent in op_intents: if ( existing_flight + and existing_flight.op_intent and existing_flight.op_intent.reference.id == op_intent.reference.id ): log( @@ -189,11 +191,12 @@ def log(msg): modifying_activated = ( existing_flight + and existing_flight.op_intent and existing_flight.op_intent.reference.state == scd_api.OperationalIntentState.Activated and op_intent.reference.state == scd_api.OperationalIntentState.Activated ) - if modifying_activated: + if modifying_activated and existing_flight and existing_flight.op_intent: preexisting_conflict = Volume4DCollection.from_interuss_scd_api( existing_flight.op_intent.details.volumes ).intersects_vol4s(v2) @@ -216,22 +219,24 @@ def op_intent_transition_valid( transition_to: scd_api.OperationalIntentState | None, ) -> bool: valid_states = { + None, scd_api.OperationalIntentState.Accepted, scd_api.OperationalIntentState.Activated, scd_api.OperationalIntentState.Nonconforming, scd_api.OperationalIntentState.Contingent, } - if transition_from is not None and transition_from not in valid_states: + if transition_from not in valid_states: raise ValueError( f"Cannot transition from state {transition_from} as it is an invalid operational intent state" ) - if transition_to is not None and transition_to not in valid_states: + if transition_to not in valid_states: raise ValueError( f"Cannot transition to state {transition_to} as it is an invalid operational intent state" ) if transition_from is None: return transition_to in { + None, scd_api.OperationalIntentState.Accepted, scd_api.OperationalIntentState.Activated, } @@ -385,6 +390,10 @@ def op_intent_from_flightinfo( def op_intent_from_flightrecord( flight: FlightRecord, method: str ) -> f3548_v21.OperationalIntent: + if not flight.op_intent: + raise RuntimeError( + "op_intent_from_flightrecord was called with a FlightRecord containing no op intent" + ) ref = flight.op_intent.reference details = f3548_v21.OperationalIntentDetails( volumes=flight.op_intent.details.volumes, @@ -423,9 +432,13 @@ def query_operational_intents( op_intent_refs = scd_client.query_operational_intent_references( utm_client, area_of_interest ) - tx = db.value + dbcontent: Database = db.value get_details_for = [] - own_flights = {f.op_intent.reference.id: f for f in tx.flights.values() if f} + own_flights = { + f.op_intent.reference.id: f + for f in dbcontent.flights.values() + if f and f.op_intent + } result = [] for op_intent_ref in op_intent_refs: if op_intent_ref.id in own_flights: @@ -434,12 +447,12 @@ def query_operational_intents( op_intent_from_flightrecord(own_flights[op_intent_ref.id], "GET") ) elif ( - op_intent_ref.id in tx.cached_operations - and tx.cached_operations[op_intent_ref.id].reference.version + op_intent_ref.id in dbcontent.cached_operations + and dbcontent.cached_operations[op_intent_ref.id].reference.version == op_intent_ref.version ): # We have a current version of this op intent cached - result.append(tx.cached_operations[op_intent_ref.id]) + result.append(dbcontent.cached_operations[op_intent_ref.id]) else: # We need to get the details for this op intent get_details_for.append(op_intent_ref) @@ -537,57 +550,63 @@ def check_op_intent( # Check the transition is valid state_transition_from = ( f3548_v21.OperationalIntentState(existing_flight.op_intent.reference.state) - if existing_flight + if existing_flight and existing_flight.op_intent else None ) state_transition_to = f3548_v21.OperationalIntentState( - new_flight.op_intent.reference.state + new_flight.op_intent.reference.state if new_flight.op_intent else None ) if not op_intent_transition_valid(state_transition_from, state_transition_to): raise PlanningError( f"Operational intent state transition from {state_transition_from} to {state_transition_to} is invalid" ) - # Check the priority is allowed in the locality - priority = priority_of(new_flight.op_intent.details) - if ( - priority > locality.highest_priority() - or priority <= locality.lowest_bound_priority() - ): - raise PlanningError( - f"Operational intent priority {priority} is outside the bounds of the locality priority range (]{locality.lowest_bound_priority()},{locality.highest_priority()}])" - ) + if new_flight.op_intent: + # Check the priority is allowed in the locality + priority = priority_of(new_flight.op_intent.details) + if ( + priority > locality.highest_priority() + or priority <= locality.lowest_bound_priority() + ): + raise PlanningError( + f"Operational intent priority {priority} is outside the bounds of the locality priority range (]{locality.lowest_bound_priority()},{locality.highest_priority()}])" + ) - if new_flight.op_intent.reference.state in ( - f3548_v21.OperationalIntentState.Accepted, - f3548_v21.OperationalIntentState.Activated, - ): - # Check for intersections if the flight is nominal + if new_flight.op_intent.reference.state in ( + f3548_v21.OperationalIntentState.Accepted, + f3548_v21.OperationalIntentState.Activated, + ): + # Check for intersections if the flight is nominal - # Check for operational intents in the DSS - log("Obtaining latest operational intent information") - v1 = Volume4DCollection.from_interuss_scd_api( - new_flight.op_intent.details.volumes - + new_flight.op_intent.details.off_nominal_volumes - ) - vol4 = v1.bounding_volume.to_f3548v21() - op_intents = query_operational_intents(locality, vol4) + # Check for operational intents in the DSS + log("Obtaining latest operational intent information") + v1 = Volume4DCollection.from_f3548v21( + (new_flight.op_intent.details.volumes or []) + + (new_flight.op_intent.details.off_nominal_volumes or []) + ) + vol4 = v1.bounding_volume.to_f3548v21() + op_intents = query_operational_intents(locality, vol4) - # Check for intersections - log( - f"Checking for intersections with {', '.join(op_intent.reference.id for op_intent in op_intents)}" - ) - has_conflicts = check_for_conflicts( - new_flight.op_intent, existing_flight, op_intents, locality, log - ) + # Check for intersections + log( + f"Checking for intersections with {', '.join(op_intent.reference.id for op_intent in op_intents)}" + ) + has_conflicts = check_for_conflicts( + new_flight.op_intent, existing_flight, op_intents, locality, log + ) + + key = [ + f3548_v21.EntityOVN(op.reference.ovn) + for op in op_intents + if op.reference.ovn is not None + ] + else: + # Flight is not nominal and therefore doesn't need to check intersections + key = [] + has_conflicts = False - key = [ - f3548_v21.EntityOVN(op.reference.ovn) - for op in op_intents - if op.reference.ovn is not None - ] else: - # Flight is not nominal and therefore doesn't need to check intersections + # Flight does not have an op intent key = [] has_conflicts = False @@ -611,6 +630,10 @@ def share_op_intent( * ConnectionError * requests.exceptions.ConnectionError """ + if not new_flight.op_intent: + raise RuntimeError( + "share_op_intent called with new_flight that is missing an op_intent" + ) # Create operational intent in DSS log("Sharing operational intent with DSS") base_url = new_flight.op_intent.reference.uss_base_url @@ -624,7 +647,7 @@ def share_op_intent( uss_base_url=base_url ), ) - if existing_flight: + if existing_flight and existing_flight.op_intent: id = existing_flight.op_intent.reference.id log(f"Updating existing operational intent {id} in DSS") result = scd_client.update_operational_intent_reference( diff --git a/monitoring/mock_uss/f3548v21/routes_scd.py b/monitoring/mock_uss/f3548v21/routes_scd.py index 59a626a7da..a687f9dc73 100644 --- a/monitoring/mock_uss/f3548v21/routes_scd.py +++ b/monitoring/mock_uss/f3548v21/routes_scd.py @@ -37,12 +37,12 @@ def scdsc_get_operational_intent_details(entityid: str): tx = db.value flight = None for f in tx.flights.values(): - if f and f.op_intent.reference.id == entityid: + if f and f.op_intent and f.op_intent.reference.id == entityid: flight = f break # If requested operational intent doesn't exist, return 404 - if flight is None: + if flight is None or flight.op_intent is None: return ( flask.jsonify( ErrorResponse( @@ -70,7 +70,7 @@ def scdsc_get_operational_intent_telemetry(entityid: str): tx = db.value flight: FlightRecord | None = None for f in tx.flights.values(): - if f and f.op_intent.reference.id == entityid: + if f and f.op_intent and f.op_intent.reference.id == entityid: flight = f break @@ -85,7 +85,7 @@ def scdsc_get_operational_intent_telemetry(entityid: str): 404, ) - elif flight.op_intent.reference.state not in { + elif flight.op_intent and flight.op_intent.reference.state not in { OperationalIntentState.Contingent, OperationalIntentState.Nonconforming, }: diff --git a/monitoring/mock_uss/flights/database.py b/monitoring/mock_uss/flights/database.py index 97ca0ea1bb..f12eb9bb97 100644 --- a/monitoring/mock_uss/flights/database.py +++ b/monitoring/mock_uss/flights/database.py @@ -15,10 +15,11 @@ class FlightRecord(ImplicitDict): - """Representation of a flight in a USS""" + """Representation of a flight in mock_uss""" + # TODO(mock_uss_flight_id): Add flight ID that is independent of op_intent flight_info: FlightInfo - op_intent: OperationalIntent + op_intent: Optional[OperationalIntent] = None mod_op_sharing_behavior: Optional[MockUssFlightBehavior] = None locked: bool = False diff --git a/monitoring/mock_uss/scd_injection/routes_injection.py b/monitoring/mock_uss/scd_injection/routes_injection.py index 764573fa67..f37831d7ce 100644 --- a/monitoring/mock_uss/scd_injection/routes_injection.py +++ b/monitoring/mock_uss/scd_injection/routes_injection.py @@ -188,6 +188,7 @@ def unsuccessful( op_intent=op_intent, mod_op_sharing_behavior=mod_op_sharing_behavior, ) + assert new_flight.op_intent # Validate request try: @@ -326,42 +327,44 @@ def unsuccessful(msg: str) -> PlanningActivityResponse: if flight is None: return unsuccessful(f"Flight {flight_id} does not exist"), 404 - # Delete operational intent from DSS - step_name = "performing unknown operation" notes: str | None = None - try: - step_name = f"deleting operational intent {flight.op_intent.reference.id} with OVN {flight.op_intent.reference.ovn} from DSS" - log(step_name) - notif_errors = delete_op_intent(flight.op_intent.reference, log) - if notif_errors: - notif_errors_messages = [ - f"{url}: {str(err)}" for url, err in notif_errors.items() - ] - notes = f"Deletion succeeded, but notification to some subscribers failed: {'; '.join(notif_errors_messages)}" + if flight.op_intent: + # Delete operational intent from DSS + step_name = "performing unknown operation" + try: + step_name = f"deleting operational intent {flight.op_intent.reference.id} with OVN {flight.op_intent.reference.ovn} from DSS" + log(step_name) + notif_errors = delete_op_intent(flight.op_intent.reference, log) + if notif_errors: + notif_errors_messages = [ + f"{url}: {str(err)}" for url, err in notif_errors.items() + ] + notes = f"Deletion succeeded, but notification to some subscribers failed: {'; '.join(notif_errors_messages)}" + log(notes) + + except (ValueError, ConnectionError) as e: + notes = f"{e.__class__.__name__} while {step_name} for flight {flight_id}: {str(e)}" log(notes) - - except (ValueError, ConnectionError) as e: - notes = ( - f"{e.__class__.__name__} while {step_name} for flight {flight_id}: {str(e)}" - ) - log(notes) - # Activity result is Failed, but we executed the activity successfully - return unsuccessful(notes), 200 - except requests.exceptions.ConnectionError as e: - notes = f"Connection error to {e.request.method} {e.request.url} while {step_name} for flight {flight_id}: {str(e)}" - log(notes) - response = unsuccessful(notes) - response["stacktrace"] = stacktrace_string(e) - # Activity result is Failed, but we executed the activity successfully - return response, 200 - except QueryError as e: - notes = f"Unexpected response from remote server while {step_name} for flight {flight_id}: {str(e)}" - log(notes) - response = unsuccessful(notes) - response["queries"] = e.queries - response["stacktrace"] = e.stacktrace - # Activity result is Failed, but we executed the activity successfully - return response, 200 + # Activity result is Failed, but we executed the activity successfully + return unsuccessful(notes), 200 + except requests.exceptions.ConnectionError as e: + if e.request: + notes = f"Connection error to {e.request.method} {e.request.url} while {step_name} for flight {flight_id}: {str(e)}" + else: + notes = f"Connection error missing .request while {step_name} for flight {flight_id}: {str(e)}" + log(notes) + response = unsuccessful(notes) + response["stacktrace"] = stacktrace_string(e) + # Activity result is Failed, but we executed the activity successfully + return response, 200 + except QueryError as e: + notes = f"Unexpected response from remote server while {step_name} for flight {flight_id}: {str(e)}" + log(notes) + response = unsuccessful(notes) + response["queries"] = e.queries + response["stacktrace"] = e.stacktrace + # Activity result is Failed, but we executed the activity successfully + return response, 200 log("Complete.") return ( @@ -440,24 +443,20 @@ def make_result(error: dict | None = None) -> ClearAreaResponse: op_intent_refs = scd_client.query_operational_intent_references( utm_client, vol4 ) - op_intent_ids = {oi.id for oi in op_intent_refs} # Try to remove all relevant flights normally for flight_id, flight in db.value.flights.items(): if flight is None: continue - # TODO: Check for intersection with flight's area rather than just relying on DSS query - if flight.op_intent.reference.id not in op_intent_ids: - continue - del_resp, _status_code = delete_flight(flight_id) if ( del_resp.activity_result == PlanningActivityResult.Completed and del_resp.flight_plan_status == FlightPlanStatus.Closed ): flights_deleted.append(flight_id) - op_intents_removed.append(flight.op_intent.reference.id) + if flight.op_intent: + op_intents_removed.append(flight.op_intent.reference.id) else: notes = f"Deleting known flight {flight_id} {del_resp.activity_result} with `flight_plan_status`={del_resp.flight_plan_status}" if "notes" in del_resp and del_resp.notes: