From eacd77b36261798711f67ed48cb862c21817d23a Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Wed, 14 Oct 2026 18:25:31 -0700 Subject: [PATCH] Add SingularResource and use for conflicting_flights --- NEXT_RELEASE_NOTES.md | 4 +- .../dev/f3548_self_contained.yaml | 1 - .../configurations/dev/message_signing.yaml | 1 - .../configurations/dev/uspace.yaml | 2 - .../definitions/baseline_a.libsonnet | 1 - monitoring/uss_qualifier/resources/README.md | 29 ++++++++--- .../uss_qualifier/resources/resource.py | 49 ++++++++++++++++++- .../suites/astm/utm/f3548_21.yaml | 10 +++- .../suites/faa/uft/message_signing.yaml | 7 ++- .../suites/uspace/flight_auth.yaml | 6 ++- .../suites/uspace/required_services.yaml | 5 +- schemas/manage_type_schemas.py | 8 +-- .../SingularResourceSpecification.json | 19 +++++++ 13 files changed, 119 insertions(+), 23 deletions(-) create mode 100644 schemas/monitoring/uss_qualifier/resources/resource/SingularResourceSpecification.json diff --git a/NEXT_RELEASE_NOTES.md b/NEXT_RELEASE_NOTES.md index 21d0564e33..31520ba97c 100644 --- a/NEXT_RELEASE_NOTES.md +++ b/NEXT_RELEASE_NOTES.md @@ -38,8 +38,10 @@ The release notes should contain at least the following sections: ## Mandatory migration tasks * For any uss_qualifier test configurations using any of the test suites `suites.astm.utm.f3548_21`, `suites.faa.uft.message_signing`, `suites.uspace.flight_auth`, or `suites.uspace.required_services`, the following resources must be changed: - * A new required resource named `conflicting_flights_provider` must be provided. It must be a `resources.resource.ResourceProvidingResource` providing `resources.flight_planning.FlightIntentsResource` instances with spatially separated flight intents for each planner combination used by `ConflictEqualPriorityNotPermitted`. One suitable implementation is `resources.geospatial.TriangularCascadeSoutheastResource` with `conflicting_flights` as its `base_resource`; see [f3548_self_contained](./monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml) for an example. The existing `conflicting_flights` resource is still required for other scenarios. + * A new required resource named `conflicting_flights_provider` must be provided. It must be a `resources.resource.ResourceProvidingResource` providing `resources.flight_planning.FlightIntentsResource` instances with spatially separated flight intents for each planner combination used by `ConflictEqualPriorityNotPermitted`. One suitable implementation is `resources.geospatial.TriangularCascadeSoutheastResource` with `conflicting_flights` as its `base_resource`; see [f3548_self_contained](./monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml) for an example. ## Optional migration tasks +* For uss_qualifier test configurations using `suites.astm.utm.f3548_21`, `suites.faa.uft.message_signing`, `suites.uspace.flight_auth`, or `suites.uspace.required_services`, the `conflicting_flights` input is now optional and its suite resource mapping can be removed. By default, `f3548_21` selects index 0 from `conflicting_flights_provider`. An explicitly supplied `conflicting_flights` still overrides this default for now. Keep any base flight resource declaration needed by the provider. + ## Important information diff --git a/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml b/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml index e54e5c3722..55ada1fe85 100644 --- a/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml +++ b/monitoring/uss_qualifier/configurations/dev/f3548_self_contained.yaml @@ -19,7 +19,6 @@ v1: prod_env_version_providers: prod_env_version_providers flight_planners: flight_planners flight_planners_to_clear: flight_planners - conflicting_flights: conflicting_flights conflicting_flights_provider: conflicting_flights_provider priority_preemption_flights_provider: conflicting_flights_provider invalid_flight_intents: invalid_flight_intents diff --git a/monitoring/uss_qualifier/configurations/dev/message_signing.yaml b/monitoring/uss_qualifier/configurations/dev/message_signing.yaml index 11933a816e..57ca9b2b5e 100644 --- a/monitoring/uss_qualifier/configurations/dev/message_signing.yaml +++ b/monitoring/uss_qualifier/configurations/dev/message_signing.yaml @@ -52,7 +52,6 @@ v1: mock_uss: mock_uss flight_planners: flight_planners combination_selector: combination_selector - conflicting_flights: che_conflicting_flights conflicting_flights_provider: che_conflicting_flights_provider invalid_flight_intents: che_invalid_flight_intents non_conflicting_flights: che_non_conflicting_flights diff --git a/monitoring/uss_qualifier/configurations/dev/uspace.yaml b/monitoring/uss_qualifier/configurations/dev/uspace.yaml index 78fe0af51a..73bcc2dc27 100644 --- a/monitoring/uss_qualifier/configurations/dev/uspace.yaml +++ b/monitoring/uss_qualifier/configurations/dev/uspace.yaml @@ -59,7 +59,6 @@ v1: test_env_version_providers: test_env_version_providers? prod_env_version_providers: prod_env_version_providers? - conflicting_flights: che_conflicting_flights conflicting_flights_provider: che_conflicting_flights_provider priority_preemption_flights_provider: che_conflicting_flights_provider invalid_flight_intents: che_invalid_flight_intents @@ -97,7 +96,6 @@ v1: test_env_version_providers: test_env_version_providers? prod_env_version_providers: prod_env_version_providers? - conflicting_flights: conflicting_flights conflicting_flights_provider: conflicting_flights_provider priority_preemption_flights_provider: priority_preemption_flights_provider invalid_flight_intents: invalid_flight_intents diff --git a/monitoring/uss_qualifier/configurations/dev/utm_implementation_us/definitions/baseline_a.libsonnet b/monitoring/uss_qualifier/configurations/dev/utm_implementation_us/definitions/baseline_a.libsonnet index 930a4e71cd..c537e4071c 100644 --- a/monitoring/uss_qualifier/configurations/dev/utm_implementation_us/definitions/baseline_a.libsonnet +++ b/monitoring/uss_qualifier/configurations/dev/utm_implementation_us/definitions/baseline_a.libsonnet @@ -22,7 +22,6 @@ function(env) { prod_env_version_providers: 'prod_env_version_providers', flight_planners: 'flight_planners', flight_planners_to_clear: 'flight_planners_to_clear', - conflicting_flights: 'conflicting_flights', conflicting_flights_provider: 'conflicting_flights_provider', priority_preemption_flights_provider: 'conflicting_flights_provider', invalid_flight_intents: 'invalid_flight_intents', diff --git a/monitoring/uss_qualifier/resources/README.md b/monitoring/uss_qualifier/resources/README.md index 00db1441b9..ab644218a0 100644 --- a/monitoring/uss_qualifier/resources/README.md +++ b/monitoring/uss_qualifier/resources/README.md @@ -35,13 +35,30 @@ Resources for a given test configuration are all declared in a single global res Dependency resource IDs may be suffixed with `?` to indicate that the dependency is optional. Explicit constructor parameters must have a default value to accept optional dependencies. Dependencies accepted through `**kwargs` are treated as having an implicit default of `None`. Missing optional dependencies are omitted from the constructor call, so they do not appear in the `kwargs` dictionary. -## Resource-modifying resources +## Resource-providing resources -A `ResourceModifyingResource` is a resource capable of spawning other resources by modifying a template/base resource according to a desired key, such as an index. This is useful when a test scenario needs multiple unique-but-related instances of a resource (e.g., distinct flights derived from a single base flight). +A `ResourceProvidingResource[SpecificationType, ResourceType]` provides resources of `ResourceType` according to a desired key, such as an index. This is useful when a test scenario needs multiple related instances of a resource (e.g., spatially separated flights derived from a single base flight). -To use a `ResourceModifyingResource`: +To use a `ResourceProvidingResource`: -1. Declare it like any other resource, with its `base_resource` dependency pointing to the resource to be modified. -2. When a variant of the base/template resource is needed, call `modify(key)` to obtain a modified copy of the base resource. Different `key` values generally produce different variants; the same `key` should produce equivalent results. +1. Declare it like any other resource, with the dependencies its implementation requires. +2. Call `provide_resource_for` with the supported keys (e.g., `provide_resource_for(index=0)`). -The base/template resource itself remains available as `base_resource` on the modifier. +## Selecting a single provided resource + +Use `resources.resource.SingularResource[ResourceType]` to place one indexed provider result in the resource pool: + +```yaml +conflicting_flights: + resource_type: resources.resource.SingularResource[resources.flight_planning.FlightIntentsResource] + dependencies: + provider: conflicting_flights_provider + specification: + index: 0 +``` + +The pool contains the actual resource returned by `provider.provide_resource_for(index=0)` rather than the SingularResource itself, so consumers can depend directly on `FlightIntentsResource`. The selected resource retains its own origin and generic type metadata. Providers must support an integer `index` key and return the declared resource type. + +## Resolving into pools + +Resource construction calls `resolve_resource()` after assigning generic type bindings. The default implementation returns `self`; selectors may override it to return the resource to expose to consumers. Resolution happens once during construction, and the result is reused by downstream dependencies. diff --git a/monitoring/uss_qualifier/resources/resource.py b/monitoring/uss_qualifier/resources/resource.py index 30d560165e..528e974299 100644 --- a/monitoring/uss_qualifier/resources/resource.py +++ b/monitoring/uss_qualifier/resources/resource.py @@ -108,6 +108,14 @@ def is_or_inherits(self, resource_type: ResourceTypeName) -> bool: self._get_type_annotation(), specified_type, self._get_typevar_map() ) + def resolve_resource(self) -> "Resource": + """Return the resource to expose in the resource pool after construction. + + Selectors may return another resource. Its type metadata and origin are + preserved, and consumers receive that resource rather than the selector. + """ + return self + class ValueResource[T: ImplicitDict](Resource[T]): value: T @@ -142,6 +150,45 @@ def _provided_resource_origin(self, key_name: str) -> str: return f"Resource for {key_name} provided by {self.resource_origin}" +class SingularResourceSpecification(ImplicitDict): + index: int + """Index of the resource to select from the provider.""" + + +class SingularResource[SingularResourceType: Resource]( + Resource[SingularResourceSpecification] +): + """Expose a single provider result as its actual resource type in the pool.""" + + _specification: SingularResourceSpecification + _provider: ResourceProvidingResource[ImplicitDict, SingularResourceType] + + def __init__( + self, + specification: SingularResourceSpecification, + resource_origin: str, + provider: ResourceProvidingResource[ImplicitDict, SingularResourceType], + ): + super().__init__(specification, resource_origin) + self._specification = specification + self._provider = provider + + def resolve_resource(self) -> SingularResourceType: + resource = self._provider.provide_resource_for(index=self._specification.index) + expected_type = substitute_typevars( + get_type_hints(SingularResource.resolve_resource)["return"], + self._get_typevar_map(), + ) + if not isinstance(resource, Resource) or not is_type_covariant_match( + resource._get_type_annotation(), expected_type, resource._get_typevar_map() + ): + raise ValueError( + f"Resource provided by {self._provider.resource_origin} for index {self._specification.index} " + f"when resolving {self.resource_origin} does not match expected type {format_type_name(expected_type)}" + ) + return resource + + class MissingResourceError(ValueError): missing_resource_name: str @@ -399,7 +446,7 @@ def _make_resource( **dependency_args, ) resource._typevar_map = decl_types.typevar_map - return resource + return resource.resolve_resource() def make_child_resources[ResourceType: Resource]( diff --git a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml index bab2138765..114ec4a0d4 100644 --- a/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml +++ b/monitoring/uss_qualifier/suites/astm/utm/f3548_21.yaml @@ -8,7 +8,9 @@ resources: dss_instances: resources.astm.f3548.v21.DSSInstancesResource? interuss_dss_instances: resources.astm.f3548.v21.DSSInstancesResource? dss_datastore_cluster: resources.interuss.datastore.DatastoreDBClusterResource? - conflicting_flights: resources.flight_planning.FlightIntentsResource + + # This input is DEPRECATED and may be removed in future versions. + conflicting_flights: resources.flight_planning.FlightIntentsResource? # This resource should provide resources.flight_planning.FlightIntentsResource instances conflicting_flights_provider: resources.resource.ResourceProvidingResource @@ -28,6 +30,12 @@ resources: system_identity: resources.versioning.SystemIdentityResource? test_exclusions: resources.dev.TestExclusionsResource? local_resources: + conflicting_flights: + resource_type: resources.resource.SingularResource[resources.flight_planning.FlightIntentsResource] + dependencies: + provider: conflicting_flights_provider + specification: + index: 0 system_identity: resource_type: resources.versioning.SystemIdentityResource specification: diff --git a/monitoring/uss_qualifier/suites/faa/uft/message_signing.yaml b/monitoring/uss_qualifier/suites/faa/uft/message_signing.yaml index d4394918a4..189e29f8d8 100644 --- a/monitoring/uss_qualifier/suites/faa/uft/message_signing.yaml +++ b/monitoring/uss_qualifier/suites/faa/uft/message_signing.yaml @@ -6,7 +6,10 @@ resources: dss: resources.astm.f3548.v21.DSSInstanceResource dss_instances: resources.astm.f3548.v21.DSSInstancesResource? dss_datastore_cluster: resources.interuss.datastore.DatastoreDBClusterResource? - conflicting_flights: resources.flight_planning.FlightIntentsResource + + # This input is DEPRECATED and may be removed in future versions. + conflicting_flights: resources.flight_planning.FlightIntentsResource? + non_conflicting_flights: resources.flight_planning.FlightIntentsResource # This resource should provide resources.flight_planning.FlightIntentsResource instances @@ -29,7 +32,7 @@ actions: suite_type: suites.astm.utm.f3548_21 resources: mock_uss: mock_uss - conflicting_flights: conflicting_flights + conflicting_flights: conflicting_flights? conflicting_flights_provider: conflicting_flights_provider non_conflicting_flights: non_conflicting_flights priority_preemption_flights_provider: priority_preemption_flights_provider? diff --git a/monitoring/uss_qualifier/suites/uspace/flight_auth.yaml b/monitoring/uss_qualifier/suites/uspace/flight_auth.yaml index 4ffa0ad23e..2e5916cdc9 100644 --- a/monitoring/uss_qualifier/suites/uspace/flight_auth.yaml +++ b/monitoring/uss_qualifier/suites/uspace/flight_auth.yaml @@ -2,7 +2,9 @@ name: U-space flight authorisation resources: test_env_version_providers: resources.versioning.VersionProvidersResource? prod_env_version_providers: resources.versioning.VersionProvidersResource? - conflicting_flights: resources.flight_planning.FlightIntentsResource + + # This input is DEPRECATED and may be removed in future versions. + conflicting_flights: resources.flight_planning.FlightIntentsResource? # This resource should provide resources.flight_planning.FlightIntentsResource instances conflicting_flights_provider: resources.resource.ResourceProvidingResource @@ -30,7 +32,7 @@ actions: resources: test_env_version_providers: test_env_version_providers? prod_env_version_providers: prod_env_version_providers? - conflicting_flights: conflicting_flights + conflicting_flights: conflicting_flights? conflicting_flights_provider: conflicting_flights_provider priority_preemption_flights_provider: priority_preemption_flights_provider? invalid_flight_intents: invalid_flight_intents diff --git a/monitoring/uss_qualifier/suites/uspace/required_services.yaml b/monitoring/uss_qualifier/suites/uspace/required_services.yaml index b944484130..e03e5f2337 100644 --- a/monitoring/uss_qualifier/suites/uspace/required_services.yaml +++ b/monitoring/uss_qualifier/suites/uspace/required_services.yaml @@ -3,7 +3,8 @@ resources: test_env_version_providers: resources.versioning.VersionProvidersResource? prod_env_version_providers: resources.versioning.VersionProvidersResource? - conflicting_flights: resources.flight_planning.FlightIntentsResource + # This input is DEPRECATED and may be removed in future versions. + conflicting_flights: resources.flight_planning.FlightIntentsResource? # This resource should provide resources.flight_planning.FlightIntentsResource instances conflicting_flights_provider: resources.resource.ResourceProvidingResource @@ -51,7 +52,7 @@ actions: resources: test_env_version_providers: test_env_version_providers? prod_env_version_providers: prod_env_version_providers? - conflicting_flights: conflicting_flights + conflicting_flights: conflicting_flights? conflicting_flights_provider: conflicting_flights_provider priority_preemption_flights_provider: priority_preemption_flights_provider? invalid_flight_intents: invalid_flight_intents diff --git a/schemas/manage_type_schemas.py b/schemas/manage_type_schemas.py index 3d9c5e5edc..8dd4b8d3e8 100644 --- a/schemas/manage_type_schemas.py +++ b/schemas/manage_type_schemas.py @@ -131,13 +131,15 @@ def _find_specifications( ): _find_specifications(member, repo, already_checked) elif inspect.isclass(member): - if getattr(member, "__parameters__", ()): - continue if issubclass(member, Resource) and member != Resource: if inspect.isabstract(member): continue spec_type = _resolve_resource_spec_type(member) - repo[fullname(spec_type)] = spec_type + # Generic resources can still have a concrete specification. + if isinstance(spec_type, type) and issubclass(spec_type, ImplicitDict): + repo[fullname(spec_type)] = spec_type + elif getattr(member, "__parameters__", ()): + continue elif issubclass(member, ActionGenerator) and member != ActionGenerator: spec_type = get_args(member.__orig_bases__[0])[0] repo[fullname(spec_type)] = spec_type diff --git a/schemas/monitoring/uss_qualifier/resources/resource/SingularResourceSpecification.json b/schemas/monitoring/uss_qualifier/resources/resource/SingularResourceSpecification.json new file mode 100644 index 0000000000..3d0639f11f --- /dev/null +++ b/schemas/monitoring/uss_qualifier/resources/resource/SingularResourceSpecification.json @@ -0,0 +1,19 @@ +{ + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/resource/SingularResourceSpecification.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "monitoring.uss_qualifier.resources.resource.SingularResourceSpecification, as defined in monitoring/uss_qualifier/resources/resource.py", + "properties": { + "$ref": { + "description": "Path to content that replaces the $ref", + "type": "string" + }, + "index": { + "description": "Index of the resource to select from the provider.", + "type": "integer" + } + }, + "required": [ + "index" + ], + "type": "object" +} \ No newline at end of file