From 7225627244b94e440020f389f779331ae70b923f Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Sun, 25 Oct 2026 00:20:58 -0400 Subject: [PATCH] runID is now taken from the first view of the current run --- clams/app/__init__.py | 46 ++++++++++++++++---------------- tests/test_clamsapp.py | 60 ++++++++++++++++++++++++++++++++++++------ 2 files changed, 76 insertions(+), 30 deletions(-) diff --git a/clams/app/__init__.py b/clams/app/__init__.py index 0f4bac3..3f1bc2b 100644 --- a/clams/app/__init__.py +++ b/clams/app/__init__.py @@ -255,8 +255,7 @@ def annotate(self, mmif: Union[str, dict, Mmif], **runtime_params: List[str]) -> warnings_view = annotated.new_view() self.sign_view(warnings_view, refined) warnings_view.metadata.warnings = issued_warnings - run_id = datetime.now() - td = run_id - t + td = datetime.now() - t runningTime = refined.get('runningTime', False) hwFetch = refined.get('hwFetch', False) runtime_recs: Dict[str, Any] = {} @@ -279,16 +278,23 @@ def annotate(self, mmif: Union[str, dict, Mmif], **runtime_params: List[str]) -> stdout=subprocess.PIPE).stdout.decode('utf-8').strip().split('\n'): name, mem = gpu.split(', ') runtime_recs['cuda'].append(self._cuda_device_name_concat(name, mem)) - for annotated_view in annotated.views: - if annotated_view.id not in existing_view_ids and annotated_view.metadata.app == str(self.metadata.identifier): + new_app_views = [v for v in annotated.views + if v.id not in existing_view_ids + and v.metadata.app == str(self.metadata.identifier)] + # all views from this run share the timestamp that mmif-python + # assigned to the first of them, serving as the "run ID" + run_id = next((v.metadata.timestamp for v in new_app_views + if v.metadata.timestamp is not None), None) + for annotated_view in new_app_views: + if run_id is not None: annotated_view.metadata.timestamp = run_id - profiling_data: Dict[str, Any] = {} - if runningTime: - profiling_data['runningTime'] = str(td) - if len(runtime_recs) > 0: - profiling_data['hardware'] = runtime_recs - if profiling_data: - annotated_view.metadata.set_additional_property('appProfiling', profiling_data) + profiling_data: Dict[str, Any] = {} + if runningTime: + profiling_data['runningTime'] = str(td) + if len(runtime_recs) > 0: + profiling_data['hardware'] = runtime_recs + if profiling_data: + annotated_view.metadata.set_additional_property('appProfiling', profiling_data) return annotated.serialize(pretty=pretty, sanitize=True) @@ -436,9 +442,11 @@ def add_parameters_to_metadata(self, view: View, parameters: dict): def set_error_view(self, mmif: Union[str, dict, Mmif], **runtime_conf: List[str]) -> Mmif: """ - A method to record an error instead of annotation results in the view - this app generated. For logging purpose, the runtime parameters used - when the error occurred must be passed as well. + A method to record an error instead of annotation results in a new + view appended to the input MMIF. Existing views are left untouched, + including views from earlier runs of the same app. For logging + purpose, the runtime parameters used when the error occurred must be + passed as well. :param mmif: input MMIF object :param runtime_conf: parameters passed to annotate when the app encountered the error @@ -448,14 +456,8 @@ def set_error_view(self, mmif: Union[str, dict, Mmif], **runtime_conf: List[str] if isinstance(mmif, (bytes, str, dict)): mmif, runtime_conf = unwrap_if_envelope(mmif, runtime_conf) mmif = Mmif(mmif) - error_view: Optional[View] = None - for view in reversed(mmif.views): - if view.metadata.app == str(self.metadata.identifier): - error_view = view - break - if error_view is None: - error_view = mmif.new_view() - self.sign_view(error_view, runtime_conf) + error_view = mmif.new_view() + self.sign_view(error_view, runtime_conf) exc_info = sys.exc_info() error_view.set_error(f'{exc_info[0]}: {exc_info[1]}', '\t\n'.join(traceback.format_tb(exc_info[2]))) diff --git a/tests/test_clamsapp.py b/tests/test_clamsapp.py index 2b7a4cf..a3ea819 100644 --- a/tests/test_clamsapp.py +++ b/tests/test_clamsapp.py @@ -5,7 +5,9 @@ import tempfile import unittest import warnings +from datetime import datetime, timezone from typing import Union +from unittest.mock import patch import jsonschema import pytest @@ -360,6 +362,31 @@ def test_run_id(self): else: self.assertEqual(second_timestamp, view.metadata.timestamp) + def test_run_id_reuses_mmif_timestamp(self): + # the run ID is the timestamp mmif-python assigned to the first view + # of the run; clams-python does not generate its own + sentinel = datetime(2001, 2, 3, 4, 5, 6, tzinfo=timezone.utc) + original_annotate = self.app._annotate + + def _annotate(mmif, **kwargs): + out = original_annotate(mmif, **kwargs) + next(reversed(out.views)).metadata.timestamp = sentinel + return out + + # serialize the sentinel the same way mmif-python does + ref = ExampleInputMMIF.get_rawmmif() + ref.new_view().metadata.timestamp = sentinel + expected = next(reversed( + Mmif(ref.serialize(), validate=False).views)).metadata.timestamp + with patch.object(self.app, '_annotate', new=_annotate): + out_mmif = Mmif(self.app.annotate(self.in_mmif)) + app_views = [v for v in out_mmif.views + if v.metadata.app == str(self.app.metadata.identifier)] + # one contentful view and one warning view + self.assertEqual(len(app_views), 2) + for view in app_views: + self.assertEqual(expected, view.metadata.timestamp) + def test_annotate_returns_invalid_mmif(self): m = Mmif(self.in_mmif) v = m.new_view() @@ -430,21 +457,38 @@ def test_refine_parameters(self): def test_error_handling(self): params = {'raise_error': ['true'], 'pretty': ['true']} - in_mmif = Mmif(self.in_mmif) try: - out_mmif = self.app.annotate(in_mmif, **params) + out_mmif = self.app.annotate(self.in_mmif, **params) except Exception as e: - out_mmif_from_str = self.app.set_error_view(self.in_mmif, **params) - out_mmif_from_mmif = self.app.set_error_view(in_mmif, **params) - self.assertEqual( - out_mmif_from_mmif.views.get_last_contentful_view(), - out_mmif_from_str.views.get_last_contentful_view()) - out_mmif = out_mmif_from_str + out_mmif = self.app.set_error_view(self.in_mmif, **params) self.assertIsNotNone(out_mmif) + self.assertEqual(len(out_mmif.views), 1) last_view: View = next(reversed(out_mmif.views)) self.assertEqual(len(last_view.metadata.contains), 0) self.assertEqual(len(last_view.metadata.error), 2) + def test_error_view_keeps_previous_run(self): + # an error in a later run of the same app must not overwrite + # the views from an earlier, successful run + first = Mmif(self.app.annotate(self.in_mmif)) + params = {'raise_error': ['true']} + try: + self.app.annotate(first.serialize(), **params) + except Exception: + out_mmif = self.app.set_error_view(first.serialize(), **params) + self.assertEqual(len(out_mmif.views), len(first.views) + 1) + for before, after in zip(first.views, out_mmif.views): + self.assertEqual(before.id, after.id) + self.assertFalse(after.has_error()) + self.assertEqual(len(before.annotations), len(after.annotations)) + self.assertEqual(before.metadata.timestamp, + after.metadata.timestamp) + error_view: View = next(reversed(out_mmif.views)) + self.assertTrue(error_view.has_error()) + self.assertEqual(len(error_view.annotations), 0) + self.assertNotIn(error_view.metadata.timestamp, + [v.metadata.timestamp for v in first.views]) + def test_empty_view_becomes_warning_view(self): class EmptyClamsApp(clams.app.ClamsApp): def _appmetadata(self):