From 1339a16585ea7cb99771d08fbfcd88df4bfc702e Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Tue, 26 May 2026 09:47:56 -0400 Subject: [PATCH] added `appTags` to app metadata, sibling patch to https://github.com/clamsproject/mmif/pull/253 --- clams/app/__init__.py | 2 ++ clams/appmetadata/__init__.py | 21 +++++++++++++++++++++ tests/test_clamsapp.py | 27 +++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/clams/app/__init__.py b/clams/app/__init__.py index 1d3f0a2..1b228b6 100644 --- a/clams/app/__init__.py +++ b/clams/app/__init__.py @@ -309,6 +309,8 @@ def sign_view(self, view: View, runtime_conf: dict) -> None: :param runtime_conf: runtime configuration of the app as k-v pairs """ view.metadata.app = str(self.metadata.identifier) + if self.metadata.app_tags: + view.metadata.set_additional_property('appTags', list(self.metadata.app_tags)) params_map = {p.name: p for p in self.metadata.parameters} if self._RAW_PARAMS_KEY in runtime_conf: for k, v in runtime_conf.items(): diff --git a/clams/appmetadata/__init__.py b/clams/appmetadata/__init__.py index fd2fe75..b6fe185 100644 --- a/clams/appmetadata/__init__.py +++ b/clams/appmetadata/__init__.py @@ -357,6 +357,14 @@ class AppMetadata(pydantic.BaseModel): None, description="(optional) A string-to-string map that can be used to store any additional metadata of the app." ) + app_tags: List[str] = pydantic.Field( + [], + description="(optional) A list of short string labels that classify what kind of work this app does " + "(e.g. task name, output profile family). Used by downstream consumers as a first-pass filter " + "for selecting views; not a substitute for inspecting actual output types and properties. " + "The values declared here are propagated by the SDK into the ``appTags`` field of every view " + "the app signs." + ) est_gpu_mem_min: int = pydantic.Field( 0, description="(optional) Minimum GPU memory required to run the app, in megabytes (MB). " @@ -472,6 +480,19 @@ def add_input_oneof(self, *inputs: Union[str, Input, vocabulary.ThingTypesBase]) newinputs.append(i) self.input.append(newinputs) + def add_app_tag(self, *tags: str) -> None: + """ + Helper method to add one or more strings to the ``app_tags`` list, + skipping any value that is already present. + + :param tags: one or more tag strings to add + """ + for tag in tags: + if not isinstance(tag, str) or not tag: + raise ValueError(f"app tag must be a non-empty string: {tag!r}") + if tag not in self.app_tags: + self.app_tags.append(tag) + def add_output(self, at_type: Union[str, vocabulary.ThingTypesBase], **properties) -> Output: """ Helper method to add an element to the ``output`` list. diff --git a/tests/test_clamsapp.py b/tests/test_clamsapp.py index 1abcbac..412aa5d 100644 --- a/tests/test_clamsapp.py +++ b/tests/test_clamsapp.py @@ -262,6 +262,33 @@ def test_sign_view(self): self.assertEqual(len(v4.metadata.appConfiguration), 6) self.assertEqual(len(v4.metadata.parameters['multivalued_param']), len(str(multiple_values))) + def test_app_tags_default_empty(self): + # apps that don't declare tags should not write an appTags field to view metadata + self.assertEqual(self.app.metadata.app_tags, []) + m = Mmif(self.in_mmif) + v = m.new_view() + self.app.sign_view(v, {}) + self.assertIsNone(v.metadata.get('appTags')) + + def test_app_tags_propagated_to_view(self): + # tags declared on app metadata should appear verbatim in signed views + self.app.metadata.add_app_tag('TemporalSegmentation', 'BarsDetection') + m = Mmif(self.in_mmif) + v = m.new_view() + self.app.sign_view(v, {}) + self.assertEqual( + v.metadata.get('appTags'), + ['TemporalSegmentation', 'BarsDetection'], + ) + + def test_add_app_tag_dedupes_and_validates(self): + self.app.metadata.add_app_tag('foo', 'bar', 'foo') + self.assertEqual(self.app.metadata.app_tags, ['foo', 'bar']) + with self.assertRaises(ValueError): + self.app.metadata.add_app_tag('') + with self.assertRaises(ValueError): + self.app.metadata.add_app_tag(123) # type: ignore[arg-type] + def test_annotate(self): # The example app is hard-coded to **always** emit version mismatch warning out_mmif = self.app.annotate(self.in_mmif)