Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 24 additions & 22 deletions clams/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {}
Expand All @@ -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)

Expand Down Expand Up @@ -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
Expand All @@ -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])))
Expand Down
60 changes: 52 additions & 8 deletions tests/test_clamsapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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):
Expand Down
Loading