From afc93cadb8d18fc17e11c26f8c5cd2ba6c3a139c Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Wed, 20 May 2026 20:50:28 +0000 Subject: [PATCH 1/5] Update naming and documentation for ResourceModifyingResource --- monitoring/uss_qualifier/resources/README.md | 10 +-- .../uss_qualifier/resources/dev/__init__.py | 6 +- .../resources/dev/test_modifier.py | 35 ++++++----- .../uss_qualifier/resources/resource.py | 29 ++++++--- .../uss_qualifier/resources/resources_test.py | 62 ++++++++++--------- ...NumberGeneratorModifierSpecification.json} | 4 +- ... => TestNumberGeneratorSpecification.json} | 4 +- 7 files changed, 84 insertions(+), 66 deletions(-) rename schemas/monitoring/uss_qualifier/resources/dev/test_modifier/{TestModifierModifierSpecification.json => TestNumberGeneratorModifierSpecification.json} (66%) rename schemas/monitoring/uss_qualifier/resources/dev/test_modifier/{TestModifierSpecification.json => TestNumberGeneratorSpecification.json} (66%) diff --git a/monitoring/uss_qualifier/resources/README.md b/monitoring/uss_qualifier/resources/README.md index e53d100d7a..e3eb1cad78 100644 --- a/monitoring/uss_qualifier/resources/README.md +++ b/monitoring/uss_qualifier/resources/README.md @@ -33,13 +33,13 @@ Resources for a given test configuration are all declared in a single global res 3. Every type of test resource must define how to create an instance of the test resource from an instance of the resource specification. -## Resource modifiers +## Resource-modifying resources -A `ResourceModifier` is a resource that wraps another resource and produces variants of it based on an integer 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 `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). -To use a `ResourceModifier`: +To use a `ResourceModifyingResource`: 1. Declare it like any other resource, with its `base_resource` dependency pointing to the resource to be modified. -2. When need, call `adjust(index)` to obtain a modified copy of the base resource. Different `index` values produce different (unique) variants; the same `index` produces equivalent results. +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. -The base resource itself remains available as `base_resource` on the modifier. +The base/template resource itself remains available as `base_resource` on the modifier. diff --git a/monitoring/uss_qualifier/resources/dev/__init__.py b/monitoring/uss_qualifier/resources/dev/__init__.py index d666e7f23e..4a3e839a57 100644 --- a/monitoring/uss_qualifier/resources/dev/__init__.py +++ b/monitoring/uss_qualifier/resources/dev/__init__.py @@ -1,4 +1,6 @@ from .noop import NoOpResource as NoOpResource from .test_exclusions import TestExclusionsResource as TestExclusionsResource -from .test_modifier import TestModifierModifierResource as TestModifierModifierResource -from .test_modifier import TestModifierResource as TestModifierResource +from .test_modifier import ( + TestNumberGeneratorModifierResource as TestNumberGeneratorModifierResource, +) +from .test_modifier import TestNumberGeneratorResource as TestNumberGeneratorResource diff --git a/monitoring/uss_qualifier/resources/dev/test_modifier.py b/monitoring/uss_qualifier/resources/dev/test_modifier.py index ba0e53fa16..d9ebe504aa 100644 --- a/monitoring/uss_qualifier/resources/dev/test_modifier.py +++ b/monitoring/uss_qualifier/resources/dev/test_modifier.py @@ -1,20 +1,23 @@ from implicitdict import ImplicitDict -from monitoring.uss_qualifier.resources.resource import Resource, ResourceModifier +from monitoring.uss_qualifier.resources.resource import ( + Resource, + ResourceModifyingResource, +) -class TestModifierSpecification(ImplicitDict): +class TestNumberGeneratorSpecification(ImplicitDict): base_id: int -class TestModifierResource(Resource[TestModifierSpecification]): - """TestModifierResource is a simple resource returing 10 number, starting from base_id. Used for unit tests.""" +class TestNumberGeneratorResource(Resource[TestNumberGeneratorSpecification]): + """A simple resource returing 10 numbers, starting from base_id. Used for unit tests.""" - _spec: TestModifierSpecification + _spec: TestNumberGeneratorSpecification def __init__( self, - specification: TestModifierSpecification, + specification: TestNumberGeneratorSpecification, resource_origin: str, ): super().__init__(specification, resource_origin) @@ -24,22 +27,24 @@ def build_ids(self) -> list[int]: return list(range(self._spec.base_id, self._spec.base_id + 10)) -class TestModifierModifierSpecification(ImplicitDict): +class TestNumberGeneratorModifierSpecification(ImplicitDict): shift_interval: int -class TestModifierModifierResource( - ResourceModifier[TestModifierModifierSpecification, TestModifierResource] +class TestNumberGeneratorModifierResource( + ResourceModifyingResource[ + TestNumberGeneratorModifierSpecification, int, TestNumberGeneratorResource + ] ): - """Modifier for a TestModifierResource. Used for unit tests.""" + """Modifier for a TestNumberGeneratorResource. Used for unit tests.""" - def adjust(self, index: int) -> TestModifierResource: + def modify(self, key: int) -> TestNumberGeneratorResource: # 'Clone' the resource with new specs - return TestModifierResource( - TestModifierSpecification( + return TestNumberGeneratorResource( + TestNumberGeneratorSpecification( base_id=self.base_resource._spec.base_id - + self._spec.shift_interval * index, + + self._spec.shift_interval * key, ), - resource_origin=self.base_resource.resource_origin, + resource_origin=self._modified_resource_origin(str(key)), ) diff --git a/monitoring/uss_qualifier/resources/resource.py b/monitoring/uss_qualifier/resources/resource.py index 5089c5a2ad..8c211f257f 100644 --- a/monitoring/uss_qualifier/resources/resource.py +++ b/monitoring/uss_qualifier/resources/resource.py @@ -41,15 +41,19 @@ def is_type(self, resource_type: str) -> bool: ResourceType = TypeVar("ResourceType", bound=Resource) +SpawnKeyType = TypeVar("SpawnKeyType") -class ResourceModifier[SpecificationType: ImplicitDict, ResourceType]( - Resource[SpecificationType], ABC -): - """A specifc type of resources that can return adjusted an resource that shall unique based on a specifc 'index'. - The underlying resource shall be a dependency named 'base_resource'. +class ResourceModifyingResource[ + SpecificationType: ImplicitDict, + SpawnKeyType, + ResourceType: Resource, +](Resource[SpecificationType], ABC): + """Resource capable of spawning ResourceType resources by modifying a template/base + ResourceType resource according to a desired key, such as an index. - Concrete subclass must implement 'adjust' as needed. + Useful for deconflicting multiple copies of a resource so many different variants of the same + test can be performed without conflicting with each other. """ _spec: SpecificationType @@ -65,12 +69,17 @@ def __init__( self._spec = specification self.base_resource = base_resource + def _modified_resource_origin(self, modification_name: str) -> str: + """Method that should be used to determine the origin of a resource spawned by this resource.""" + return f"Modification {modification_name} of {self.base_resource.resource_origin} by {self.resource_origin}" + @abstractmethod - def adjust(self, index: int) -> ResourceType: - """ - Return a new instance of the base resource, modified to be unique based on 'index' value. + def modify(self, key: SpawnKeyType) -> ResourceType: + """Spawn a new resource formed by modifying the template/base resource according to the provided key. + + Different `key` values generally produce different variants; the same `key` should produce equivalent results. """ - pass + raise NotImplementedError() class MissingResourceError(ValueError): diff --git a/monitoring/uss_qualifier/resources/resources_test.py b/monitoring/uss_qualifier/resources/resources_test.py index c80bff6cbc..f36c354e6f 100644 --- a/monitoring/uss_qualifier/resources/resources_test.py +++ b/monitoring/uss_qualifier/resources/resources_test.py @@ -5,70 +5,72 @@ ResourceID, ) from monitoring.uss_qualifier.resources.dev.test_modifier import ( - TestModifierModifierSpecification, - TestModifierSpecification, + TestNumberGeneratorModifierSpecification, + TestNumberGeneratorSpecification, ) from monitoring.uss_qualifier.resources.resource import create_resources -class TestResourceModifier(unittest.TestCase): - def _build_test_modifier_declaration( +class TestModifierResource(unittest.TestCase): + def _build_number_generator_declaration( self, base_id ) -> dict[ResourceID, ResourceDeclaration]: return { - "test": ResourceDeclaration( - resource_type="resources.dev.TestModifierResource", - specification=TestModifierSpecification(base_id=base_id), + "number_generator": ResourceDeclaration( + resource_type="resources.dev.TestNumberGeneratorResource", + specification=TestNumberGeneratorSpecification(base_id=base_id), ) } - def _build_test_modifier_modifier_declaration( + def _build_modifier_declaration( self, base_id, shift_interval ) -> dict[ResourceID, ResourceDeclaration]: return { - "test": self._build_test_modifier_declaration(base_id)["test"], - "test_modifier": ResourceDeclaration( - resource_type="resources.dev.TestModifierModifierResource", - specification=TestModifierModifierSpecification( + "number_generator": self._build_number_generator_declaration(base_id)[ + "number_generator" + ], + "modifier": ResourceDeclaration( + resource_type="resources.dev.TestNumberGeneratorModifierResource", + specification=TestNumberGeneratorModifierSpecification( shift_interval=shift_interval ), dependencies={ - "base_resource": "test", + "base_resource": "number_generator", }, ), } def test_base_resource(self): """Test basic usage of the resource""" - declaration = self._build_test_modifier_declaration(42) + declaration = self._build_number_generator_declaration(42) - resources = create_resources(declaration, "test", True) - assert "test" in resources + resources = create_resources(declaration, "unittest", True) + assert "number_generator" in resources - resource = resources["test"] + resource = resources["number_generator"] assert resource.build_ids() == [42, 43, 44, 45, 46, 47, 48, 49, 50, 51] def test_base_resource_base_id(self): """Test that base id works as expected""" - declaration = self._build_test_modifier_declaration(52) + declaration = self._build_number_generator_declaration(52) - resources = create_resources(declaration, "test", True) - assert "test" in resources + resources = create_resources(declaration, "unittest", True) + assert "number_generator" in resources - resource = resources["test"] + resource = resources["number_generator"] assert resource.build_ids() == [52, 53, 54, 55, 56, 57, 58, 59, 60, 61] def test_modifier_resource(self): - """Test basic usage of the resource modifier""" - declaration = self._build_test_modifier_modifier_declaration(42, 10) + """Test basic usage of the resource modifier resource""" + declaration = self._build_modifier_declaration(42, 10) - resources = create_resources(declaration, "test", True) - assert "test_modifier" in resources + resources = create_resources(declaration, "unittest", True) + assert "modifier" in resources - resource = resources["test_modifier"] + resource = resources["modifier"] assert resource.adjust(0).build_ids() == [ 42, @@ -97,12 +99,12 @@ def test_modifier_resource(self): def test_modifier_resource_shift(self): """Test shift usage of the resource modifier""" - declaration = self._build_test_modifier_modifier_declaration(42, 20) + declaration = self._build_modifier_declaration(42, 20) - resources = create_resources(declaration, "test", True) - assert "test_modifier" in resources + resources = create_resources(declaration, "unittest", True) + assert "modifier" in resources - resource = resources["test_modifier"] + resource = resources["modifier"] assert resource.adjust(0).build_ids() == [ 42, diff --git a/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierModifierSpecification.json b/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestNumberGeneratorModifierSpecification.json similarity index 66% rename from schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierModifierSpecification.json rename to schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestNumberGeneratorModifierSpecification.json index 24a9d04c65..670c9bd715 100644 --- a/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierModifierSpecification.json +++ b/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestNumberGeneratorModifierSpecification.json @@ -1,7 +1,7 @@ { - "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierModifierSpecification.json", + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestNumberGeneratorModifierSpecification.json", "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "monitoring.uss_qualifier.resources.dev.test_modifier.TestModifierModifierSpecification, as defined in monitoring/uss_qualifier/resources/dev/test_modifier.py", + "description": "monitoring.uss_qualifier.resources.dev.test_modifier.TestNumberGeneratorModifierSpecification, as defined in monitoring/uss_qualifier/resources/dev/test_modifier.py", "properties": { "$ref": { "description": "Path to content that replaces the $ref", diff --git a/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierSpecification.json b/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestNumberGeneratorSpecification.json similarity index 66% rename from schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierSpecification.json rename to schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestNumberGeneratorSpecification.json index 1824bdad7e..126e38b16e 100644 --- a/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierSpecification.json +++ b/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestNumberGeneratorSpecification.json @@ -1,7 +1,7 @@ { - "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestModifierSpecification.json", + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestNumberGeneratorSpecification.json", "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "monitoring.uss_qualifier.resources.dev.test_modifier.TestModifierSpecification, as defined in monitoring/uss_qualifier/resources/dev/test_modifier.py", + "description": "monitoring.uss_qualifier.resources.dev.test_modifier.TestNumberGeneratorSpecification, as defined in monitoring/uss_qualifier/resources/dev/test_modifier.py", "properties": { "$ref": { "description": "Path to content that replaces the $ref", From 8ad1a4dd6d92542ac4878216a69129a429c0a35e Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Wed, 20 May 2026 21:51:01 +0000 Subject: [PATCH 2/5] Fix unit test error and warnings --- .../uss_qualifier/resources/dev/__init__.py | 4 ++-- .../resources/dev/test_modifier.py | 22 +++++++++---------- .../uss_qualifier/resources/resources_test.py | 20 ++++++++--------- ...NumberGeneratorModifierSpecification.json} | 4 ++-- ...json => NumberGeneratorSpecification.json} | 4 ++-- 5 files changed, 27 insertions(+), 27 deletions(-) rename schemas/monitoring/uss_qualifier/resources/dev/test_modifier/{TestNumberGeneratorModifierSpecification.json => NumberGeneratorModifierSpecification.json} (66%) rename schemas/monitoring/uss_qualifier/resources/dev/test_modifier/{TestNumberGeneratorSpecification.json => NumberGeneratorSpecification.json} (66%) diff --git a/monitoring/uss_qualifier/resources/dev/__init__.py b/monitoring/uss_qualifier/resources/dev/__init__.py index 4a3e839a57..7b572b03de 100644 --- a/monitoring/uss_qualifier/resources/dev/__init__.py +++ b/monitoring/uss_qualifier/resources/dev/__init__.py @@ -1,6 +1,6 @@ from .noop import NoOpResource as NoOpResource from .test_exclusions import TestExclusionsResource as TestExclusionsResource from .test_modifier import ( - TestNumberGeneratorModifierResource as TestNumberGeneratorModifierResource, + NumberGeneratorModifierResource as NumberGeneratorModifierResource, ) -from .test_modifier import TestNumberGeneratorResource as TestNumberGeneratorResource +from .test_modifier import NumberGeneratorResource as NumberGeneratorResource diff --git a/monitoring/uss_qualifier/resources/dev/test_modifier.py b/monitoring/uss_qualifier/resources/dev/test_modifier.py index d9ebe504aa..c4d6143a57 100644 --- a/monitoring/uss_qualifier/resources/dev/test_modifier.py +++ b/monitoring/uss_qualifier/resources/dev/test_modifier.py @@ -6,18 +6,18 @@ ) -class TestNumberGeneratorSpecification(ImplicitDict): +class NumberGeneratorSpecification(ImplicitDict): base_id: int -class TestNumberGeneratorResource(Resource[TestNumberGeneratorSpecification]): +class NumberGeneratorResource(Resource[NumberGeneratorSpecification]): """A simple resource returing 10 numbers, starting from base_id. Used for unit tests.""" - _spec: TestNumberGeneratorSpecification + _spec: NumberGeneratorSpecification def __init__( self, - specification: TestNumberGeneratorSpecification, + specification: NumberGeneratorSpecification, resource_origin: str, ): super().__init__(specification, resource_origin) @@ -27,22 +27,22 @@ def build_ids(self) -> list[int]: return list(range(self._spec.base_id, self._spec.base_id + 10)) -class TestNumberGeneratorModifierSpecification(ImplicitDict): +class NumberGeneratorModifierSpecification(ImplicitDict): shift_interval: int -class TestNumberGeneratorModifierResource( +class NumberGeneratorModifierResource( ResourceModifyingResource[ - TestNumberGeneratorModifierSpecification, int, TestNumberGeneratorResource + NumberGeneratorModifierSpecification, int, NumberGeneratorResource ] ): - """Modifier for a TestNumberGeneratorResource. Used for unit tests.""" + """Modifier for a NumberGeneratorResource. Used for unit tests.""" - def modify(self, key: int) -> TestNumberGeneratorResource: + def modify(self, key: int) -> NumberGeneratorResource: # 'Clone' the resource with new specs - return TestNumberGeneratorResource( - TestNumberGeneratorSpecification( + return NumberGeneratorResource( + NumberGeneratorSpecification( base_id=self.base_resource._spec.base_id + self._spec.shift_interval * key, ), diff --git a/monitoring/uss_qualifier/resources/resources_test.py b/monitoring/uss_qualifier/resources/resources_test.py index f36c354e6f..9d49b11334 100644 --- a/monitoring/uss_qualifier/resources/resources_test.py +++ b/monitoring/uss_qualifier/resources/resources_test.py @@ -5,8 +5,8 @@ ResourceID, ) from monitoring.uss_qualifier.resources.dev.test_modifier import ( - TestNumberGeneratorModifierSpecification, - TestNumberGeneratorSpecification, + NumberGeneratorModifierSpecification, + NumberGeneratorSpecification, ) from monitoring.uss_qualifier.resources.resource import create_resources @@ -17,8 +17,8 @@ def _build_number_generator_declaration( ) -> dict[ResourceID, ResourceDeclaration]: return { "number_generator": ResourceDeclaration( - resource_type="resources.dev.TestNumberGeneratorResource", - specification=TestNumberGeneratorSpecification(base_id=base_id), + resource_type="resources.dev.NumberGeneratorResource", + specification=NumberGeneratorSpecification(base_id=base_id), ) } @@ -30,8 +30,8 @@ def _build_modifier_declaration( "number_generator" ], "modifier": ResourceDeclaration( - resource_type="resources.dev.TestNumberGeneratorModifierResource", - specification=TestNumberGeneratorModifierSpecification( + resource_type="resources.dev.NumberGeneratorModifierResource", + specification=NumberGeneratorModifierSpecification( shift_interval=shift_interval ), dependencies={ @@ -72,7 +72,7 @@ def test_modifier_resource(self): resource = resources["modifier"] - assert resource.adjust(0).build_ids() == [ + assert resource.modify(0).build_ids() == [ 42, 43, 44, @@ -84,7 +84,7 @@ def test_modifier_resource(self): 50, 51, ] - assert resource.adjust(1).build_ids() == [ + assert resource.modify(1).build_ids() == [ 52, 53, 54, @@ -106,7 +106,7 @@ def test_modifier_resource_shift(self): resource = resources["modifier"] - assert resource.adjust(0).build_ids() == [ + assert resource.modify(0).build_ids() == [ 42, 43, 44, @@ -118,7 +118,7 @@ def test_modifier_resource_shift(self): 50, 51, ] - assert resource.adjust(1).build_ids() == [ + assert resource.modify(1).build_ids() == [ 62, 63, 64, diff --git a/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestNumberGeneratorModifierSpecification.json b/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/NumberGeneratorModifierSpecification.json similarity index 66% rename from schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestNumberGeneratorModifierSpecification.json rename to schemas/monitoring/uss_qualifier/resources/dev/test_modifier/NumberGeneratorModifierSpecification.json index 670c9bd715..a767c59bda 100644 --- a/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestNumberGeneratorModifierSpecification.json +++ b/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/NumberGeneratorModifierSpecification.json @@ -1,7 +1,7 @@ { - "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestNumberGeneratorModifierSpecification.json", + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/NumberGeneratorModifierSpecification.json", "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "monitoring.uss_qualifier.resources.dev.test_modifier.TestNumberGeneratorModifierSpecification, as defined in monitoring/uss_qualifier/resources/dev/test_modifier.py", + "description": "monitoring.uss_qualifier.resources.dev.test_modifier.NumberGeneratorModifierSpecification, as defined in monitoring/uss_qualifier/resources/dev/test_modifier.py", "properties": { "$ref": { "description": "Path to content that replaces the $ref", diff --git a/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestNumberGeneratorSpecification.json b/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/NumberGeneratorSpecification.json similarity index 66% rename from schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestNumberGeneratorSpecification.json rename to schemas/monitoring/uss_qualifier/resources/dev/test_modifier/NumberGeneratorSpecification.json index 126e38b16e..b1f66f3b5e 100644 --- a/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestNumberGeneratorSpecification.json +++ b/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/NumberGeneratorSpecification.json @@ -1,7 +1,7 @@ { - "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestNumberGeneratorSpecification.json", + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/NumberGeneratorSpecification.json", "$schema": "https://json-schema.org/draft/2020-12/schema", - "description": "monitoring.uss_qualifier.resources.dev.test_modifier.TestNumberGeneratorSpecification, as defined in monitoring/uss_qualifier/resources/dev/test_modifier.py", + "description": "monitoring.uss_qualifier.resources.dev.test_modifier.NumberGeneratorSpecification, as defined in monitoring/uss_qualifier/resources/dev/test_modifier.py", "properties": { "$ref": { "description": "Path to content that replaces the $ref", From cb3c10b5433383bddb8fa89cc6e02fdb0678aa7c Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Thu, 21 May 2026 21:28:53 +0000 Subject: [PATCH 3/5] Add ResourceProvidingResource base class per comment thread --- .../resources/dev/test_modifier.py | 2 +- .../uss_qualifier/resources/resource.py | 27 +++++++++++++++---- .../uss_qualifier/resources/resources_test.py | 8 +++--- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/monitoring/uss_qualifier/resources/dev/test_modifier.py b/monitoring/uss_qualifier/resources/dev/test_modifier.py index c4d6143a57..0fdbd405e1 100644 --- a/monitoring/uss_qualifier/resources/dev/test_modifier.py +++ b/monitoring/uss_qualifier/resources/dev/test_modifier.py @@ -38,7 +38,7 @@ class NumberGeneratorModifierResource( ): """Modifier for a NumberGeneratorResource. Used for unit tests.""" - def modify(self, key: int) -> NumberGeneratorResource: + def provide_resource_for(self, key: int) -> NumberGeneratorResource: # 'Clone' the resource with new specs return NumberGeneratorResource( diff --git a/monitoring/uss_qualifier/resources/resource.py b/monitoring/uss_qualifier/resources/resource.py index 8c211f257f..6d487b02bc 100644 --- a/monitoring/uss_qualifier/resources/resource.py +++ b/monitoring/uss_qualifier/resources/resource.py @@ -44,12 +44,29 @@ def is_type(self, resource_type: str) -> bool: SpawnKeyType = TypeVar("SpawnKeyType") -class ResourceModifyingResource[ +class ResourceProvidingResource[ SpecificationType: ImplicitDict, SpawnKeyType, ResourceType: Resource, ](Resource[SpecificationType], ABC): - """Resource capable of spawning ResourceType resources by modifying a template/base + """Resource capable of spawning ResourceType resources according to a desired key, such as an index.""" + + @abstractmethod + def provide_resource_for(self, key: SpawnKeyType) -> ResourceType: + """Provide a resource corresponding with the provided key.""" + raise NotImplementedError() + + def _provided_resource_origin(self, key_name: str) -> str: + """Method that should generally be used to describe the origin of a resource provided by this resource.""" + return f"Resource for {key_name} provided by {self.resource_origin}" + + +class ResourceModifyingResource[ + SpecificationType: ImplicitDict, + SpawnKeyType, + ResourceType: Resource, +](ResourceProvidingResource[SpecificationType, SpawnKeyType, ResourceType], ABC): + """Resource capable of providing ResourceType resources by modifying a template/base ResourceType resource according to a desired key, such as an index. Useful for deconflicting multiple copies of a resource so many different variants of the same @@ -70,12 +87,12 @@ def __init__( self.base_resource = base_resource def _modified_resource_origin(self, modification_name: str) -> str: - """Method that should be used to determine the origin of a resource spawned by this resource.""" + """Method that should generally be used to describe the origin of a resource provided by this resource.""" return f"Modification {modification_name} of {self.base_resource.resource_origin} by {self.resource_origin}" @abstractmethod - def modify(self, key: SpawnKeyType) -> ResourceType: - """Spawn a new resource formed by modifying the template/base resource according to the provided key. + def provide_resource_for(self, key: SpawnKeyType) -> ResourceType: + """Provide a resource formed by modifying the template/base resource according to the provided key. Different `key` values generally produce different variants; the same `key` should produce equivalent results. """ diff --git a/monitoring/uss_qualifier/resources/resources_test.py b/monitoring/uss_qualifier/resources/resources_test.py index 9d49b11334..cb7a8b486a 100644 --- a/monitoring/uss_qualifier/resources/resources_test.py +++ b/monitoring/uss_qualifier/resources/resources_test.py @@ -72,7 +72,7 @@ def test_modifier_resource(self): resource = resources["modifier"] - assert resource.modify(0).build_ids() == [ + assert resource.provide_resource_for(0).build_ids() == [ 42, 43, 44, @@ -84,7 +84,7 @@ def test_modifier_resource(self): 50, 51, ] - assert resource.modify(1).build_ids() == [ + assert resource.provide_resource_for(1).build_ids() == [ 52, 53, 54, @@ -106,7 +106,7 @@ def test_modifier_resource_shift(self): resource = resources["modifier"] - assert resource.modify(0).build_ids() == [ + assert resource.provide_resource_for(0).build_ids() == [ 42, 43, 44, @@ -118,7 +118,7 @@ def test_modifier_resource_shift(self): 50, 51, ] - assert resource.modify(1).build_ids() == [ + assert resource.provide_resource_for(1).build_ids() == [ 62, 63, 64, From 2fb55072a8135799537bb041b42f47bfb8fced44 Mon Sep 17 00:00:00 2001 From: Benjamin Pelletier Date: Tue, 26 May 2026 18:21:08 +0000 Subject: [PATCH 4/5] Apply @the-glu suggestions --- .../resources/dev/test_modifier.py | 32 +++++++++++--- .../uss_qualifier/resources/resource.py | 44 +------------------ .../uss_qualifier/resources/resources_test.py | 8 ++-- 3 files changed, 31 insertions(+), 53 deletions(-) diff --git a/monitoring/uss_qualifier/resources/dev/test_modifier.py b/monitoring/uss_qualifier/resources/dev/test_modifier.py index 0fdbd405e1..e8c73dfca2 100644 --- a/monitoring/uss_qualifier/resources/dev/test_modifier.py +++ b/monitoring/uss_qualifier/resources/dev/test_modifier.py @@ -2,7 +2,7 @@ from monitoring.uss_qualifier.resources.resource import ( Resource, - ResourceModifyingResource, + ResourceProvidingResource, ) @@ -32,19 +32,37 @@ class NumberGeneratorModifierSpecification(ImplicitDict): class NumberGeneratorModifierResource( - ResourceModifyingResource[ - NumberGeneratorModifierSpecification, int, NumberGeneratorResource + ResourceProvidingResource[ + NumberGeneratorModifierSpecification, NumberGeneratorResource ] ): """Modifier for a NumberGeneratorResource. Used for unit tests.""" - def provide_resource_for(self, key: int) -> NumberGeneratorResource: + _spec: NumberGeneratorModifierSpecification + base_resource: NumberGeneratorResource - # 'Clone' the resource with new specs + def __init__( + self, + specification: NumberGeneratorModifierSpecification, + resource_origin: str, + base_resource: NumberGeneratorResource, + ): + super().__init__(specification, resource_origin) + self._spec = specification + self.base_resource = base_resource + + def _modified_resource_origin(self, index: int) -> str: + return f"Modification {index} of {self.base_resource.resource_origin} by {self.resource_origin}" + + def provide_resource_for(self, **kwargs) -> NumberGeneratorResource: + index = kwargs["index"] + assert isinstance(index, int) + + # 'Clone' the base resource with new specs return NumberGeneratorResource( NumberGeneratorSpecification( base_id=self.base_resource._spec.base_id - + self._spec.shift_interval * key, + + self._spec.shift_interval * index, ), - resource_origin=self._modified_resource_origin(str(key)), + resource_origin=self._modified_resource_origin(index), ) diff --git a/monitoring/uss_qualifier/resources/resource.py b/monitoring/uss_qualifier/resources/resource.py index 6d487b02bc..b91ff764aa 100644 --- a/monitoring/uss_qualifier/resources/resource.py +++ b/monitoring/uss_qualifier/resources/resource.py @@ -41,19 +41,17 @@ def is_type(self, resource_type: str) -> bool: ResourceType = TypeVar("ResourceType", bound=Resource) -SpawnKeyType = TypeVar("SpawnKeyType") class ResourceProvidingResource[ SpecificationType: ImplicitDict, - SpawnKeyType, ResourceType: Resource, ](Resource[SpecificationType], ABC): """Resource capable of spawning ResourceType resources according to a desired key, such as an index.""" @abstractmethod - def provide_resource_for(self, key: SpawnKeyType) -> ResourceType: - """Provide a resource corresponding with the provided key.""" + def provide_resource_for(self, **kwargs) -> ResourceType: + """Provide a resource corresponding with the provided key(s) (e.g., index, USS pair, etc).""" raise NotImplementedError() def _provided_resource_origin(self, key_name: str) -> str: @@ -61,44 +59,6 @@ def _provided_resource_origin(self, key_name: str) -> str: return f"Resource for {key_name} provided by {self.resource_origin}" -class ResourceModifyingResource[ - SpecificationType: ImplicitDict, - SpawnKeyType, - ResourceType: Resource, -](ResourceProvidingResource[SpecificationType, SpawnKeyType, ResourceType], ABC): - """Resource capable of providing ResourceType resources by modifying a template/base - ResourceType resource according to a desired key, such as an index. - - Useful for deconflicting multiple copies of a resource so many different variants of the same - test can be performed without conflicting with each other. - """ - - _spec: SpecificationType - base_resource: ResourceType - - def __init__( - self, - specification: SpecificationType, - resource_origin: str, - base_resource: ResourceType, - ): - super().__init__(specification, resource_origin) - self._spec = specification - self.base_resource = base_resource - - def _modified_resource_origin(self, modification_name: str) -> str: - """Method that should generally be used to describe the origin of a resource provided by this resource.""" - return f"Modification {modification_name} of {self.base_resource.resource_origin} by {self.resource_origin}" - - @abstractmethod - def provide_resource_for(self, key: SpawnKeyType) -> ResourceType: - """Provide a resource formed by modifying the template/base resource according to the provided key. - - Different `key` values generally produce different variants; the same `key` should produce equivalent results. - """ - raise NotImplementedError() - - class MissingResourceError(ValueError): missing_resource_name: str diff --git a/monitoring/uss_qualifier/resources/resources_test.py b/monitoring/uss_qualifier/resources/resources_test.py index cb7a8b486a..87e39e42df 100644 --- a/monitoring/uss_qualifier/resources/resources_test.py +++ b/monitoring/uss_qualifier/resources/resources_test.py @@ -72,7 +72,7 @@ def test_modifier_resource(self): resource = resources["modifier"] - assert resource.provide_resource_for(0).build_ids() == [ + assert resource.provide_resource_for(index=0).build_ids() == [ 42, 43, 44, @@ -84,7 +84,7 @@ def test_modifier_resource(self): 50, 51, ] - assert resource.provide_resource_for(1).build_ids() == [ + assert resource.provide_resource_for(index=1).build_ids() == [ 52, 53, 54, @@ -106,7 +106,7 @@ def test_modifier_resource_shift(self): resource = resources["modifier"] - assert resource.provide_resource_for(0).build_ids() == [ + assert resource.provide_resource_for(index=0).build_ids() == [ 42, 43, 44, @@ -118,7 +118,7 @@ def test_modifier_resource_shift(self): 50, 51, ] - assert resource.provide_resource_for(1).build_ids() == [ + assert resource.provide_resource_for(index=1).build_ids() == [ 62, 63, 64, From 5cb5a06685c6abdf9b17c5dd6ced8ab8955170c4 Mon Sep 17 00:00:00 2001 From: Maximilien Cuony Date: Wed, 27 May 2026 17:33:05 +0200 Subject: [PATCH 5/5] [uss_qualifier/resource] Add base class for geospatial modifier --- .basedpyright/baseline.json | 8 --- .../uss_qualifier/resources/dev/__init__.py | 2 + .../resources/dev/test_modifier.py | 59 ++++++++++++++++ .../uss_qualifier/resources/modifiers.py | 67 +++++++++++++++++++ .../uss_qualifier/resources/modifiers_test.py | 61 +++++++++++++++++ .../uss_qualifier/resources/resource.py | 22 +++--- schemas/manage_type_schemas.py | 25 ++++++- .../TestSquareSpecification.json | 22 ++++++ .../GeospatialModifierSpecification.json | 18 +++++ 9 files changed, 267 insertions(+), 17 deletions(-) create mode 100644 monitoring/uss_qualifier/resources/modifiers.py create mode 100644 monitoring/uss_qualifier/resources/modifiers_test.py create mode 100644 schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestSquareSpecification.json create mode 100644 schemas/monitoring/uss_qualifier/resources/modifiers/GeospatialModifierSpecification.json diff --git a/.basedpyright/baseline.json b/.basedpyright/baseline.json index f5950f3aca..bd2377dceb 100644 --- a/.basedpyright/baseline.json +++ b/.basedpyright/baseline.json @@ -19113,14 +19113,6 @@ "lineCount": 1 } }, - { - "code": "reportAttributeAccessIssue", - "range": { - "startColumn": 44, - "endColumn": 58, - "lineCount": 1 - } - }, { "code": "reportCallIssue", "range": { diff --git a/monitoring/uss_qualifier/resources/dev/__init__.py b/monitoring/uss_qualifier/resources/dev/__init__.py index 7b572b03de..b8d8ddade7 100644 --- a/monitoring/uss_qualifier/resources/dev/__init__.py +++ b/monitoring/uss_qualifier/resources/dev/__init__.py @@ -4,3 +4,5 @@ NumberGeneratorModifierResource as NumberGeneratorModifierResource, ) from .test_modifier import NumberGeneratorResource as NumberGeneratorResource +from .test_modifier import TestSquareModifier as TestSquareModifier +from .test_modifier import TestSquareResource as TestSquareResource diff --git a/monitoring/uss_qualifier/resources/dev/test_modifier.py b/monitoring/uss_qualifier/resources/dev/test_modifier.py index e8c73dfca2..1838b494c8 100644 --- a/monitoring/uss_qualifier/resources/dev/test_modifier.py +++ b/monitoring/uss_qualifier/resources/dev/test_modifier.py @@ -1,5 +1,10 @@ from implicitdict import ImplicitDict +from monitoring.monitorlib.geo import LatLngBoundingBox +from monitoring.uss_qualifier.resources.modifiers import ( + GeospatialModifier, + GeospatialResource, +) from monitoring.uss_qualifier.resources.resource import ( Resource, ResourceProvidingResource, @@ -66,3 +71,57 @@ def provide_resource_for(self, **kwargs) -> NumberGeneratorResource: ), resource_origin=self._modified_resource_origin(index), ) + + +class TestSquareSpecification(ImplicitDict): + lat_center: float + lng_center: float + + +class TestSquareResource(GeospatialResource, Resource[TestSquareSpecification]): + """1km x 1km square centered at (lat_center, lng_center). Used for unit tests.""" + + SQUARE_SIDE_M = 1000.0 + + _spec: TestSquareSpecification + + def __init__( + self, + specification: TestSquareSpecification, + resource_origin: str, + ): + super().__init__(specification, resource_origin) + self._spec = specification + + def get_extents(self) -> LatLngBoundingBox: + point = LatLngBoundingBox( + lat_min=self._spec.lat_center, + lat_max=self._spec.lat_center, + lng_min=self._spec.lng_center, + lng_max=self._spec.lng_center, + ) + return point.expand( + north_meters=self.SQUARE_SIDE_M / 2, + east_meters=self.SQUARE_SIDE_M / 2, + south_meters=self.SQUARE_SIDE_M / 2, + west_meters=self.SQUARE_SIDE_M / 2, + ) + + def move(self, meters_east: float, meters_north: float) -> "TestSquareResource": + shifted = self.get_extents().expand( + north_meters=meters_north, + east_meters=meters_east, + south_meters=-meters_north, + west_meters=-meters_east, + ) + return TestSquareResource( + TestSquareSpecification( + lat_center=(shifted.lat_min + shifted.lat_max) / 2, + lng_center=(shifted.lng_min + shifted.lng_max) / 2, + ), + resource_origin=self.resource_origin, + ) + + +class TestSquareModifier(GeospatialModifier[TestSquareResource]): + pass diff --git a/monitoring/uss_qualifier/resources/modifiers.py b/monitoring/uss_qualifier/resources/modifiers.py new file mode 100644 index 0000000000..9571f93a04 --- /dev/null +++ b/monitoring/uss_qualifier/resources/modifiers.py @@ -0,0 +1,67 @@ +from abc import ABC, abstractmethod +from math import isqrt +from typing import Self + +from implicitdict import ImplicitDict + +from monitoring.monitorlib.geo import LatLngBoundingBox, flatten +from monitoring.uss_qualifier.resources.resource import ( + Resource, + ResourceProvidingResource, +) + + +class GeospatialResource(Resource, ABC): + @abstractmethod + def get_extents(self) -> LatLngBoundingBox: + pass + + @abstractmethod + def move(self, meters_east: float, meters_north: float) -> Self: + pass + + +class GeospatialModifierSpecification(ImplicitDict): + meters_east_margin: float = 1000 + meters_north_margin: float = 1000 + + +class GeospatialModifier[GeospatialResourceType: GeospatialResource]( + ResourceProvidingResource[GeospatialModifierSpecification, GeospatialResourceType] +): + base_resource: GeospatialResourceType + + def __init__( + self, + specification: GeospatialModifierSpecification, + resource_origin: str, + base_resource: GeospatialResourceType, + ): + super().__init__(specification, resource_origin) + self._spec = specification + self.base_resource = base_resource + + def provide_resource_for(self, **kwargs) -> GeospatialResourceType: + + if "index" not in kwargs: + raise ValueError("Need an index") + + index = kwargs["index"] + assert isinstance(index, int) + + # Make a grid based on index: + # x -> + # y 0 1 3 6 + # | 2 4 7 + # v 5 8 + # 9 + k = (isqrt(1 + 8 * index) - 1) // 2 + offset = index - k * (k + 1) // 2 + x = k - offset + y = offset + + rect = self.base_resource.get_extents().to_latlngrect() + width_m, height_m = flatten(rect.lo(), rect.hi()) + width_m += self._spec.meters_east_margin + height_m += self._spec.meters_north_margin + return self.base_resource.move(x * width_m, y * height_m) diff --git a/monitoring/uss_qualifier/resources/modifiers_test.py b/monitoring/uss_qualifier/resources/modifiers_test.py new file mode 100644 index 0000000000..63600e26f5 --- /dev/null +++ b/monitoring/uss_qualifier/resources/modifiers_test.py @@ -0,0 +1,61 @@ +import unittest + +from monitoring.monitorlib.geo import area_of_latlngrect +from monitoring.uss_qualifier.resources.definitions import ( + ResourceDeclaration, + ResourceID, +) +from monitoring.uss_qualifier.resources.dev.test_modifier import ( + TestSquareSpecification, +) +from monitoring.uss_qualifier.resources.modifiers import ( + GeospatialModifierSpecification, +) +from monitoring.uss_qualifier.resources.resource import create_resources + + +class TestGeospatialModifier(unittest.TestCase): + def _build_declarations(self) -> dict[ResourceID, ResourceDeclaration]: + return { + "square": ResourceDeclaration( + resource_type="resources.dev.TestSquareResource", + specification=TestSquareSpecification(lat_center=46.5, lng_center=6.5), + ), + "square_modifier": ResourceDeclaration( + resource_type="resources.dev.TestSquareModifier", + specification=GeospatialModifierSpecification(), + dependencies={ + "base_resource": "square", + }, + ), + } + + def test_overlap_only_for_same_index(self): + resources = create_resources(self._build_declarations(), "test", True) + modifier = resources["square_modifier"] + + extents = [ + modifier.provide_resource_for(index=i).get_extents() for i in range(11) + ] + square_area = ( + resources["square"].SQUARE_SIDE_M * resources["square"].SQUARE_SIDE_M + ) + + for i in range(11): + for j in range(11): + rect_i = extents[i].to_latlngrect() + rect_j = extents[j].to_latlngrect() + overlap = area_of_latlngrect(rect_i.intersection(rect_j)) + if i == j: + assert ( + overlap > 0.99 * square_area + ), ( # Use 99% to compensate for errors + f"index {i}: self-overlap area {overlap:.2f}m² " + f"expected ~{square_area:.2f}m²" + ) + else: + assert ( + overlap < 0.01 * square_area + ), ( # Use 1% to compensate for errors + f"indices {i},{j}: unexpected overlap area {overlap:.2f}m²" + ) diff --git a/monitoring/uss_qualifier/resources/resource.py b/monitoring/uss_qualifier/resources/resource.py index b91ff764aa..cb4b3b3e63 100644 --- a/monitoring/uss_qualifier/resources/resource.py +++ b/monitoring/uss_qualifier/resources/resource.py @@ -173,15 +173,21 @@ def get_resource_types( constructor_signature = get_type_hints(resource_type.__init__) - # Resolve generic type vars + # Resolve generic type vars, walking up the inheritance chain + def _collect(cls: type, mapping: dict) -> None: + for base in getattr(cls, "__orig_bases__", ()): + origin = get_origin(base) + if origin is None: + continue + params = getattr(origin, "__parameters__", ()) + for param, arg in zip(params, get_args(base)): + resolved = mapping.get(arg, arg) + if not isinstance(resolved, TypeVar): + mapping[param] = resolved + _collect(origin, mapping) + typevar_map: dict = {} - for base in getattr(resource_type, "__orig_bases__", ()): - params = getattr(get_origin(base), "__type_params__", None) or getattr( - get_origin(base), "__parameters__", () - ) - for param, arg in zip(params, get_args(base)): - if not isinstance(arg, TypeVar): - typevar_map[param] = arg + _collect(resource_type, typevar_map) constructor_signature = { name: typevar_map.get(t, t) for name, t in constructor_signature.items() } diff --git a/schemas/manage_type_schemas.py b/schemas/manage_type_schemas.py index 7fbbde3073..bd4c4028ed 100644 --- a/schemas/manage_type_schemas.py +++ b/schemas/manage_type_schemas.py @@ -87,6 +87,29 @@ def _make_type_schemas( ) +def _resolve_resource_spec_type(cls: type) -> type: + """Find the spec type bound to Resource[Spec] for a Resource subclass, + resolving TypeVars through intermediate generic bases (e.g., ResourceModifier).""" + + def walk(c: type, subst: dict): + for base in getattr(c, "__orig_bases__", ()): + origin = get_origin(base) + if origin is None: + continue + args = tuple(subst.get(a, a) for a in get_args(base)) + if origin is Resource: + return args[0] + result = walk(origin, dict(zip(origin.__parameters__, args))) + if result is not None: + return result + return None + + result = walk(cls, {}) + if result is None: + raise ValueError(f"Could not resolve Resource specification type for {cls}") + return result + + def _find_specifications( module, repo: dict[str, type[ImplicitDict]], @@ -107,7 +130,7 @@ def _find_specifications( if issubclass(member, Resource) and member != Resource: if inspect.isabstract(member): continue - spec_type = get_args(member.__orig_bases__[0])[0] + spec_type = _resolve_resource_spec_type(member) repo[fullname(spec_type)] = spec_type elif issubclass(member, ActionGenerator) and member != ActionGenerator: spec_type = get_args(member.__orig_bases__[0])[0] diff --git a/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestSquareSpecification.json b/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestSquareSpecification.json new file mode 100644 index 0000000000..ead3fcd30b --- /dev/null +++ b/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestSquareSpecification.json @@ -0,0 +1,22 @@ +{ + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/dev/test_modifier/TestSquareSpecification.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "monitoring.uss_qualifier.resources.dev.test_modifier.TestSquareSpecification, as defined in monitoring/uss_qualifier/resources/dev/test_modifier.py", + "properties": { + "$ref": { + "description": "Path to content that replaces the $ref", + "type": "string" + }, + "lat_center": { + "type": "number" + }, + "lng_center": { + "type": "number" + } + }, + "required": [ + "lat_center", + "lng_center" + ], + "type": "object" +} \ No newline at end of file diff --git a/schemas/monitoring/uss_qualifier/resources/modifiers/GeospatialModifierSpecification.json b/schemas/monitoring/uss_qualifier/resources/modifiers/GeospatialModifierSpecification.json new file mode 100644 index 0000000000..26be305caa --- /dev/null +++ b/schemas/monitoring/uss_qualifier/resources/modifiers/GeospatialModifierSpecification.json @@ -0,0 +1,18 @@ +{ + "$id": "https://github.com/interuss/monitoring/blob/main/schemas/monitoring/uss_qualifier/resources/modifiers/GeospatialModifierSpecification.json", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "description": "monitoring.uss_qualifier.resources.modifiers.GeospatialModifierSpecification, as defined in monitoring/uss_qualifier/resources/modifiers.py", + "properties": { + "$ref": { + "description": "Path to content that replaces the $ref", + "type": "string" + }, + "meters_east_margin": { + "type": "number" + }, + "meters_north_margin": { + "type": "number" + } + }, + "type": "object" +} \ No newline at end of file