mmif-python 1.0.16__tar.gz → 1.0.18__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {mmif_python-1.0.16 → mmif_python-1.0.18}/PKG-INFO +1 -1
  2. mmif_python-1.0.18/VERSION +1 -0
  3. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/serialize/annotation.py +50 -7
  4. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/serialize/mmif.py +144 -94
  5. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/serialize/model.py +6 -6
  6. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/serialize/view.py +38 -17
  7. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/utils/video_document_helper.py +6 -6
  8. mmif_python-1.0.18/mmif/ver/__init__.py +2 -0
  9. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/vocabulary/annotation_types.py +2 -2
  10. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/vocabulary/base_types.py +1 -1
  11. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/vocabulary/document_types.py +2 -2
  12. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif_docloc_http/__init__.py +4 -0
  13. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif_python.egg-info/PKG-INFO +1 -1
  14. {mmif_python-1.0.16 → mmif_python-1.0.18}/tests/test_serialize.py +68 -32
  15. {mmif_python-1.0.16 → mmif_python-1.0.18}/tests/test_utils.py +2 -3
  16. mmif_python-1.0.16/VERSION +0 -1
  17. mmif_python-1.0.16/mmif/ver/__init__.py +0 -2
  18. {mmif_python-1.0.16 → mmif_python-1.0.18}/LICENSE +0 -0
  19. {mmif_python-1.0.16 → mmif_python-1.0.18}/MANIFEST.in +0 -0
  20. {mmif_python-1.0.16 → mmif_python-1.0.18}/README.md +0 -0
  21. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/__init__.py +0 -0
  22. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/res/__init__.py +0 -0
  23. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/res/clams.vocabulary.yaml +0 -0
  24. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/res/do-not-edit.txt +0 -0
  25. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/res/mmif.json +0 -0
  26. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/serialize/__init__.py +0 -0
  27. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/utils/__init__.py +0 -0
  28. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/utils/sequence_helper.py +0 -0
  29. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/utils/text_document_helper.py +0 -0
  30. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/utils/timeunit_helper.py +0 -0
  31. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/ver/do-not-edit.txt +0 -0
  32. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/vocabulary/__init__.py +0 -0
  33. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif/vocabulary/do-not-edit.txt +0 -0
  34. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif_python.egg-info/SOURCES.txt +0 -0
  35. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif_python.egg-info/dependency_links.txt +0 -0
  36. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif_python.egg-info/requires.txt +0 -0
  37. {mmif_python-1.0.16 → mmif_python-1.0.18}/mmif_python.egg-info/top_level.txt +0 -0
  38. {mmif_python-1.0.16 → mmif_python-1.0.18}/requirements.cv +0 -0
  39. {mmif_python-1.0.16 → mmif_python-1.0.18}/requirements.seq +0 -0
  40. {mmif_python-1.0.16 → mmif_python-1.0.18}/requirements.txt +0 -0
  41. {mmif_python-1.0.16 → mmif_python-1.0.18}/setup.cfg +0 -0
  42. {mmif_python-1.0.16 → mmif_python-1.0.18}/setup.py +0 -0
  43. {mmif_python-1.0.16 → mmif_python-1.0.18}/tests/test_versioncompat.py +0 -0
  44. {mmif_python-1.0.16 → mmif_python-1.0.18}/tests/test_vocab.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mmif-python
3
- Version: 1.0.16
3
+ Version: 1.0.18
4
4
  Summary: Python implementation of MultiMedia Interchange Format specification. (https://mmif.clams.ai)
5
5
  Home-page: https://mmif.clams.ai
6
6
  Author: Brandeis Lab for Linguistics and Computation
@@ -0,0 +1 @@
1
+ 1.0.18
@@ -14,7 +14,7 @@ import pkgutil
14
14
  import re
15
15
  import typing
16
16
  import warnings
17
- from typing import Union, Dict, List, Optional, Iterator, MutableMapping, TypeVar
17
+ from typing import Union, Dict, Optional, Iterator, MutableMapping, TypeVar
18
18
  from urllib.parse import urlparse
19
19
 
20
20
  from mmif.vocabulary import ThingTypesBase, DocumentTypesBase
@@ -46,12 +46,13 @@ class Annotation(MmifObject):
46
46
  MmifObject that represents an annotation in a MMIF view.
47
47
  """
48
48
 
49
- def __init__(self, anno_obj: Optional[Union[bytes, str, dict]] = None) -> None:
49
+ def __init__(self, anno_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
50
50
  self._type: ThingTypesBase = ThingTypesBase('')
51
51
  # to store the parent view ID
52
52
  self._parent_view_id = ''
53
53
  self._props_ephemeral: AnnotationProperties = AnnotationProperties()
54
- self.reserved_names.update(('_parent_view_id', '_props_ephemeral'))
54
+ self._alignments = {} # to hold alignment information (Alignment anno long_id -> aligned anno long_id)
55
+ self.reserved_names.update(('_parent_view_id', '_props_ephemeral', '_alignments'))
55
56
  if not hasattr(self, 'properties'): # don't overwrite DocumentProperties on super() call
56
57
  self.properties: AnnotationProperties = AnnotationProperties()
57
58
  self._attribute_classes = {'properties': AnnotationProperties}
@@ -59,6 +60,9 @@ class Annotation(MmifObject):
59
60
  self._required_attributes = ["_type", "properties"]
60
61
  super().__init__(anno_obj)
61
62
 
63
+ def __hash__(self):
64
+ return hash(self.serialize())
65
+
62
66
  def _deserialize(self, input_dict: dict) -> None:
63
67
  self.at_type = input_dict.pop('_type', '')
64
68
  # TODO (krim @ 6/1/21): If annotation IDs must follow a certain string format,
@@ -69,6 +73,40 @@ class Annotation(MmifObject):
69
73
  for k, v in self.properties.items():
70
74
  self._add_prop_aliases(k, v)
71
75
 
76
+ def _cache_alignment(self, alignment_ann: 'Annotation', alignedto_ann: 'Annotation') -> None:
77
+ """
78
+ Cache alignment information. This cache will not be serialized.
79
+
80
+ :param alignment_ann: the Alignment annotation that has this annotation on one side
81
+ :param alignedto_ann: the annotation that this annotation is aligned to (other side of Alignment)
82
+ """
83
+ self._alignments[alignment_ann] = alignedto_ann
84
+
85
+ def aligned_to_by(self, alignment: 'Annotation') -> Optional['Annotation']:
86
+ """
87
+ Retrieves the other side of ``Alignment`` annotation that has this annotation on one side.
88
+
89
+ :param alignment: ``Alignment`` annotation that has this annotation on one side
90
+ :return: the annotation that this annotation is aligned to (other side of ``Alignment``),
91
+ or None if this annotation is not used in the ``Alignment``.
92
+ """
93
+ return self._alignments.get(alignment)
94
+
95
+ def get_all_aligned(self) -> Iterator['Annotation']:
96
+ """
97
+ Generator to iterate through all alignments and aligned annotations. Note that this generator will yield
98
+ the `Alignment` annotations as well. Every odd-numbered yield will be an `Alignment` annotation, and every
99
+ even-numbered yield will be the aligned annotation. If there's a specific annotation type that you're looking
100
+ for, you need to filter the generated results outside.
101
+
102
+ :return: yields the alignment annotation and the aligned annotation.
103
+ The order is decided by the order of appearance of Alignment annotations in the MMIF
104
+ """
105
+ for alignment, aligned in self._alignments.items():
106
+ yield alignment
107
+ yield aligned
108
+
109
+
72
110
  def _add_prop_aliases(self, key_to_add, val_to_add):
73
111
  """
74
112
  Method to handle aliases of the same property.
@@ -163,6 +201,7 @@ class Annotation(MmifObject):
163
201
  value: Union[PRMTV_TYPES, LIST_PRMTV, LIST_LIST_PRMTV, DICT_PRMTV, DICT_LIST_PRMTV]) -> None:
164
202
  """
165
203
  Adds a property to the annotation's properties.
204
+
166
205
  :param name: the name of the property
167
206
  :param value: the property's desired value
168
207
  :return: None
@@ -241,7 +280,7 @@ class Document(Annotation):
241
280
 
242
281
  :param document_obj: the JSON data that defines the document
243
282
  """
244
- def __init__(self, doc_obj: Optional[Union[bytes, str, dict]] = None) -> None:
283
+ def __init__(self, doc_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
245
284
  # see https://github.com/clamsproject/mmif-python/issues/226 for discussion
246
285
  # around the use of these three dictionaries
247
286
  # (names changed since, `existing` >> `ephemeral` and `temporary` >> `pending`)
@@ -284,6 +323,10 @@ class Document(Annotation):
284
323
  only keep the latest value (in order of appearances in views list) of
285
324
  the property, effectively overwriting the previous values.
286
325
  """
326
+ # we don't checking if this k-v already exists in _original (new props) or _ephemeral (read from existing MMIF)
327
+ # because it is impossible to keep the _original updated when a new annotation is added (via `new_annotation`)
328
+ # without look across other views and top-level documents list. Also see
329
+ # ``mmif.serialize.mmif.Mmif.generate_capital_annotations`` for where the "de-duplication" happens.
287
330
  if name == "text":
288
331
  self.properties.text = Text(value)
289
332
  elif name == "mime":
@@ -434,7 +477,7 @@ class AnnotationProperties(MmifObject, MutableMapping[str, T]):
434
477
  else:
435
478
  return self._unnamed_attributes[key]
436
479
 
437
- def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None) -> None:
480
+ def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
438
481
  self.id: str = ''
439
482
  # any individual at_type (subclassing this class) can have its own set of required attributes
440
483
  self._required_attributes = ["id"]
@@ -451,7 +494,7 @@ class DocumentProperties(AnnotationProperties):
451
494
  :param mmif_obj: the JSON data that defines the properties
452
495
  """
453
496
 
454
- def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None) -> None:
497
+ def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
455
498
  self.mime: str = ''
456
499
  # note the trailing underscore here. I wanted to use the name `location`
457
500
  # for @property in this class and `Document` class, so had to use a diff
@@ -572,7 +615,7 @@ class DocumentProperties(AnnotationProperties):
572
615
 
573
616
  class Text(MmifObject):
574
617
 
575
- def __init__(self, text_obj: Optional[Union[bytes, str, dict]] = None) -> None:
618
+ def __init__(self, text_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
576
619
  self._value: str = ''
577
620
  self._language: str = ''
578
621
  self.disallow_additional_properties()
@@ -10,16 +10,16 @@ import math
10
10
  import warnings
11
11
  from collections import defaultdict
12
12
  from datetime import datetime
13
- from typing import List, Union, Optional, Dict, cast, Iterator, Tuple
13
+ from typing import List, Union, Optional, Dict, cast, Iterator
14
14
 
15
15
  import jsonschema.validators
16
16
 
17
17
  import mmif
18
18
  from mmif import ThingTypesBase
19
+ from mmif.serialize.annotation import Annotation, Document
20
+ from mmif.serialize.model import MmifObject, DataList
21
+ from mmif.serialize.view import View
19
22
  from mmif.vocabulary import AnnotationTypes, DocumentTypes
20
- from .annotation import Annotation, Document
21
- from .model import MmifObject, DataList
22
- from .view import View
23
23
 
24
24
  __all__ = ['Mmif']
25
25
 
@@ -31,7 +31,7 @@ class MmifMetadata(MmifObject):
31
31
  :param metadata_obj: the JSON data
32
32
  """
33
33
 
34
- def __init__(self, metadata_obj: Optional[Union[bytes, str, dict]] = None) -> None:
34
+ def __init__(self, metadata_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
35
35
  # TODO (krim @ 10/7/20): there could be a better name and a better way to give a value to this
36
36
  self.mmif: str = f"http://mmif.clams.ai/{mmif.__specver__}"
37
37
  self._required_attributes = ["mmif"]
@@ -81,7 +81,9 @@ class ViewsList(DataList[View]):
81
81
  """
82
82
  _items: Dict[str, View]
83
83
 
84
- def __init__(self, mmif_obj: Optional[Union[bytes, str, list]] = None):
84
+ def __init__(self, mmif_obj: Optional[Union[bytes, str, list]] = None, parent_mmif=None, *_):
85
+ self._parent_mmif = parent_mmif
86
+ self.reserved_names.update(("_parent_mmif", "_id_counts"))
85
87
  super().__init__(mmif_obj)
86
88
 
87
89
  def _deserialize(self, input_list: list) -> None: # pytype: disable=signature-mismatch
@@ -93,7 +95,7 @@ class ViewsList(DataList[View]):
93
95
  :return: None
94
96
  """
95
97
  if input_list:
96
- self._items = {item['id']: View(item) for item in input_list}
98
+ self._items = {item['id']: View(item, self._parent_mmif) for item in input_list}
97
99
 
98
100
  def append(self, value: View, overwrite=False) -> None:
99
101
  """
@@ -113,13 +115,24 @@ class ViewsList(DataList[View]):
113
115
  """
114
116
  super()._append_with_key(value.id, value, overwrite)
115
117
 
116
- def get_last(self) -> Optional[View]:
118
+ def get_last_contentful_view(self) -> Optional[View]:
117
119
  """
118
- Returns the last view appended to the list.
120
+ Returns the last view that is contentful, i.e., has no error or warning .
119
121
  """
120
122
  for view in reversed(self._items.values()):
121
123
  if 'error' not in view.metadata and 'warning' not in view.metadata:
122
124
  return view
125
+
126
+ def get_last_view(self) -> Optional[View]:
127
+ """
128
+ Returns the last view appended.
129
+ """
130
+ if self._items:
131
+ return self._items[list(self._items.keys())[-1]]
132
+
133
+ def get_last(self) -> Optional[View]:
134
+ warnings.warn('get_last() is deprecated, use get_last_contentful_view() instead.', DeprecationWarning)
135
+ return self.get_last_contentful_view()
123
136
 
124
137
 
125
138
  class Mmif(MmifObject):
@@ -205,6 +218,7 @@ class Mmif(MmifObject):
205
218
  """
206
219
  super()._deserialize(input_dict)
207
220
  for view in self.views:
221
+ view._parent_mmif = self
208
222
  # this dict will be populated with properties
209
223
  # that are not encoded in individual annotations objects themselves
210
224
  extrinsic_props = defaultdict(dict)
@@ -212,6 +226,7 @@ class Mmif(MmifObject):
212
226
  for prop_key, prop_value in type_lv_props.items():
213
227
  extrinsic_props[at_type][prop_key] = prop_value
214
228
  for ann in view.get_annotations():
229
+ ## for "capital" Annotation properties
215
230
  # first add all extrinsic properties to the Annotation objects
216
231
  # as "ephemeral" properties
217
232
  for prop_key, prop_value in extrinsic_props[ann.at_type].items():
@@ -226,13 +241,44 @@ class Mmif(MmifObject):
226
241
  except KeyError:
227
242
  warnings.warn(f"Annotation {ann.id} (in view {view.id}) has a document ID {doc_id} that "
228
243
  f"does not exist in the MMIF object. Skipping.", RuntimeWarning)
229
- # lastly, add quick access to `start` and `end` values if the annotation is using `targets` property
244
+
245
+ ## caching start and end points for time-based annotations
246
+ # add quick access to `start` and `end` values if the annotation is using `targets` property
230
247
  if 'targets' in ann.properties:
231
248
  if 'start' in ann.properties or 'end' in ann.properties:
232
- raise ValueError(f"Annotation {ann.id} (in view {view.id}) has `targes` and `start`/`end/` "
249
+ raise ValueError(f"Annotation {ann.id} (in view {view.id}) has `targets` and `start`/`end/` "
233
250
  f"properties at the same time. Annotation anchors are ambiguous.")
234
251
  ann._props_ephemeral['start'] = self._get_linear_anchor_point(ann, start=True)
235
252
  ann._props_ephemeral['end'] = self._get_linear_anchor_point(ann, start=False)
253
+
254
+ ## caching alignments
255
+ if ann.at_type == AnnotationTypes.Alignment:
256
+ self._cache_alignment(ann)
257
+
258
+ def _cache_alignment(self, alignment_ann: Annotation):
259
+ view = self.views.get(alignment_ann.parent)
260
+ if view is None:
261
+ warnings.warn(f"Alignment {alignment_ann.long_id} doesn't have a parent view, but it should.", RuntimeWarning)
262
+ return
263
+
264
+ ## caching alignments
265
+ def _desprately_search_annotation_object(ann_short_id):
266
+ ann_long_id = f"{view.id}{self.id_delimiter}{ann_short_id}"
267
+ try:
268
+ return self.__getitem__(ann_long_id)
269
+ except KeyError:
270
+ return self.__getitem__(ann_short_id)
271
+
272
+ if all(map(lambda x: x in alignment_ann.properties, ('source', 'target'))):
273
+ source_ann = _desprately_search_annotation_object(alignment_ann.get('source'))
274
+ target_ann = _desprately_search_annotation_object(alignment_ann.get('target'))
275
+ if isinstance(source_ann, Annotation) and isinstance(target_ann, Annotation):
276
+ source_ann._cache_alignment(alignment_ann, target_ann)
277
+ target_ann._cache_alignment(alignment_ann, source_ann)
278
+ else:
279
+ warnings.warn(
280
+ f"Alignment {alignment_ann.long_id} has `source` and `target` properties that do not point to Annotation objects.",
281
+ RuntimeWarning)
236
282
 
237
283
  def generate_capital_annotations(self):
238
284
  """
@@ -243,13 +289,23 @@ class Mmif(MmifObject):
243
289
  See https://github.com/clamsproject/mmif-python/issues/226 for rationale
244
290
  behind this behavior and discussion.
245
291
  """
246
- # this view will be the default kitchen sink for all generated annotations
247
- last_view = self.views.get_last()
292
+ # this view will be the default kitchen sink for all generated annotations.
293
+ last_view = self.views.get_last_contentful_view()
294
+
248
295
  # proceed only when there's at least one view
249
296
  if last_view:
297
+
298
+ # this app name is used to check a view is generated by the "currently running" app.
299
+ # knowing the currently running app is important so that properties of `Document` objects generated by the
300
+ # current app can be properly recorded inside the `Document` objects (since they are "writable" to the
301
+ # current app), instead of being recorded in a separate `Annotation` object.
302
+ current_app = last_view.metadata.app
303
+
250
304
  # to avoid duplicate property recording, this will be populated with
251
305
  # existing Annotation objects from all existing views
252
306
  existing_anns = defaultdict(lambda: defaultdict(dict))
307
+ # ideally, if we can "de-duplicate" props at `add_property()` time, that'd be more efficient,
308
+ # but that is impossible without looking for the target `document` across other views and top documents list
253
309
 
254
310
  # new properties to record in the current serialization call
255
311
  anns_to_write = defaultdict(dict)
@@ -261,20 +317,30 @@ class Mmif(MmifObject):
261
317
  for ann in view.get_annotations(AnnotationTypes.Annotation):
262
318
  if doc_id is None:
263
319
  doc_id = ann.get_property('document')
320
+ # only if we are sure that the document ID is unique across all views... (with v_id prefix)
321
+ # TODO (krim @ 7/15/24): update id checking once https://github.com/clamsproject/mmif/issues/228 is resolved
322
+ if not any([doc_id == doc.id for doc in self.documents]) and Mmif.id_delimiter not in doc_id:
323
+ doc_id = f"{view.id}{Mmif.id_delimiter}{doc_id}"
264
324
  existing_anns[doc_id].update(ann.properties)
265
325
  for doc in view.get_documents():
266
- anns_to_write[doc.id].update(doc._props_pending)
326
+ anns_to_write[doc.long_id].update(doc._props_pending)
267
327
  for doc in self.documents:
268
- anns_to_write[doc.id].update(doc._props_pending)
328
+ anns_to_write[doc.long_id].update(doc._props_pending)
269
329
  # additional iteration of views, to find a proper view to add the
270
330
  # generated annotations. If none found, use the last view as the kitchen sink
271
331
  last_view_for_docs = defaultdict(lambda: last_view)
272
332
  doc_ids = set(anns_to_write.keys())
273
333
  for doc_id in doc_ids:
334
+ if len(last_view.annotations) == 0:
335
+ # meaning, this new app didn't generate any annotation except for these document properties
336
+ # thus, we should add capital annotations to the last (empty) view
337
+ last_view_for_docs[doc_id] = last_view
338
+ break
274
339
  for view in reversed(self.views):
275
340
  # first try to find out if this view "contains" any annotation to the doc
276
341
  # then, check for individual annotations
277
- if [cont for cont in view.metadata.contains.values() if cont.get('document', None) == doc_id] \
342
+ # TODO (krim @ 7/15/24): update id checking once https://github.com/clamsproject/mmif/issues/228 is resolved
343
+ if [cont for cont in view.metadata.contains.values() if doc_id.endswith(cont.get('document', 'TODO:this endswith test is a temporal solution we use until long_id is forced everywhere'))] \
278
344
  or list(view.get_annotations(document=doc_id)):
279
345
  last_view_for_docs[doc_id] = view
280
346
  break
@@ -288,14 +354,18 @@ class Mmif(MmifObject):
288
354
  if k != 'id' and existing_anns[doc_id][k] != v:
289
355
  props[k] = v
290
356
  if props:
291
- if len(anns_to_write) == 1:
292
- # if there's only one document, we can record the doc_id in the contains metadata
293
- last_view_for_docs[doc_id].metadata.new_contain(AnnotationTypes.Annotation, document=doc_id)
294
- props.pop('document', None)
357
+ view_to_write = last_view_for_docs[doc_id]
358
+ if view_to_write.metadata.app == current_app and view_to_write.annotations.get(doc_id) is not None:
359
+ view_to_write.get_document_by_id(doc_id).properties.update(props)
295
360
  else:
296
- # otherwise, doc_id needs to be recorded in the annotation property
297
- props['document'] = doc_id
298
- last_view_for_docs[doc_id].new_annotation(AnnotationTypes.Annotation, **props)
361
+ if len(anns_to_write) == 1:
362
+ # if there's only one document, we can record the doc_id in the contains metadata
363
+ view_to_write.metadata.new_contain(AnnotationTypes.Annotation, document=doc_id)
364
+ props.pop('document', None)
365
+ else:
366
+ # otherwise, doc_id needs to be recorded in the annotation property
367
+ props['document'] = doc_id
368
+ view_to_write.new_annotation(AnnotationTypes.Annotation, **props)
299
369
 
300
370
  def sanitize(self):
301
371
  """
@@ -339,7 +409,7 @@ class Mmif(MmifObject):
339
409
  new_view = View()
340
410
  new_view.id = self.new_view_id()
341
411
  new_view.metadata.timestamp = datetime.now()
342
- self.views.append(new_view)
412
+ self.add_view(new_view)
343
413
  return new_view
344
414
 
345
415
  def add_view(self, view: View, overwrite=False) -> None:
@@ -353,6 +423,7 @@ class Mmif(MmifObject):
353
423
  an existing view with the same ID
354
424
  :return: None
355
425
  """
426
+ view._parent_mmif = self
356
427
  self.views.append(view, overwrite)
357
428
 
358
429
  def add_document(self, document: Document, overwrite=False) -> None:
@@ -555,7 +626,7 @@ class Mmif(MmifObject):
555
626
 
556
627
  get_views_with_error = get_all_views_with_error
557
628
 
558
- def get_all_views_contain(self, at_types: Union[ThingTypesBase, str, List[Union[str, ThingTypesBase]]]) -> List[View]:
629
+ def get_all_views_contain(self, *at_types: Union[ThingTypesBase, str]) -> List[View]:
559
630
  """
560
631
  Returns the list of all views in the MMIF if given types
561
632
  are present in that view's 'contains' metadata.
@@ -563,11 +634,8 @@ class Mmif(MmifObject):
563
634
  :param at_types: a list of types or just a type to check for. When given more than one types, all types must be found.
564
635
  :return: the list of views that contain the type
565
636
  """
566
- if isinstance(at_types, list):
567
- return [view for view in self.views
568
- if all(map(lambda x: x in view.metadata.contains, at_types))]
569
- else:
570
- return [view for view in self.views if at_types in view.metadata.contains]
637
+ return [view for view in self.views
638
+ if all(map(lambda x: x in view.metadata.contains, at_types))]
571
639
 
572
640
  get_views_contain = get_all_views_contain
573
641
 
@@ -610,72 +678,54 @@ class Mmif(MmifObject):
610
678
  return view
611
679
  return None
612
680
 
613
- def _is_in_time_between(self, start: Union[int, float], end: Union[int, float], annotation: Annotation) -> bool:
614
- s, e = self.get_start(annotation), self.get_end(annotation)
615
- return (s < start < e) or (s > start and e < end) or (s < end < e)
681
+ def _is_in_time_range(self, ann: Annotation, range_s: Union[int, float], range_e: Union[int, float]) -> bool:
682
+ """
683
+ Checks if the annotation is anchored within the given time range. Any overlap is considered included.
684
+
685
+ :param ann: the Annotation object to check, must be time-based itself or anchored to time-based annotations
686
+ :param range_s: the start time point of the range (in milliseconds)
687
+ :param range_e: the end time point of the range (in milliseconds)
688
+
689
+ :return: True if the annotation is anchored within the time range, False otherwise
690
+ """
691
+ ann_s, ann_e = self.get_start(ann), self.get_end(ann)
692
+ return (ann_s < range_s < ann_e) or (ann_s < range_e < ann_e) or (ann_s > range_s and ann_e < range_e)
693
+
694
+ def get_annotations_between_time(self, start: Union[int, float], end: Union[int, float], time_unit: str = "ms",
695
+ at_types: List[Union[ThingTypesBase, str]] = []) -> Iterator[Annotation]:
696
+ """
697
+ Finds annotations that are anchored between the given time points.
616
698
 
617
- def _handle_time_unit(self, input_unit: str, ann_unit: str,
618
- start: int, end: int) -> Tuple[Union[int, float, str], Union[int, float, str]]:
699
+ :param start: the start time point in the unit of `input_unit`
700
+ :param end: the end time point in the unit of `input_unit`
701
+ :param time_unit: the unit of the input time points. Default is `ms`.
702
+ :param at_types: a list of annotation types to filter with. Any type in this list will be included in the return.
703
+ :return: an iterator of Annotation objects that are anchored between the given time points
704
+ """
705
+ assert start < end, f"Start time point must be smaller than the end time point, given {start} and {end}"
706
+ assert start >= 0, f"Start time point must be non-negative, given {start}"
707
+ assert end >= 0, f"End time point must be non-negative, given {end}"
708
+
619
709
  from mmif.utils.timeunit_helper import convert
620
- start = convert(start, input_unit, ann_unit, 1)
621
- end = convert(end, input_unit, ann_unit, 1)
622
- return start, end
623
-
624
- def get_annotations_between_time(self, start: int, end: int, time_unit: str = "milliseconds") -> Iterator[Annotation]:
625
- """
626
- Version: 1.0
627
- Returns all 'Token' annotations aligned with 'TimeFrame' annotations sorted by start time within start and end time
628
- Note: this function only works for mmif object obtained from Whisper-wrapper
629
-
630
- :param start: the start time
631
- :param end: the end time
632
- :param time_unit: the time unit, either string "milliseconds" or "seconds", defaults to "milliseconds"
633
- :return: a generator of 'Token' annotations
634
- """
635
- assert start <= end, "Start time must be less than end time"
636
- assert start >= 0, "Start time must be greater than or equal to zero"
637
- assert end >= 0, "End time must be greater than or equal to zero"
638
- # 0. Initialize container and helper method
639
- valid_tf_anns = []
640
- tf_to_anns = defaultdict(list)
641
-
642
- # 1. find all views that contain the type of TF
643
- views = self.get_all_views_contain([AnnotationTypes.TimeFrame, AnnotationTypes.Alignment])
644
-
645
- # 2. For each view, extract annotations that satisfy conditions that are TF/TP and fall into time interval
646
- for view in views:
647
- # Make sure time unit stay at the same level
648
- start_time, end_time = self._handle_time_unit(time_unit, view.metadata.contains.get(AnnotationTypes.TimeFrame)["timeUnit"],
649
- start, end)
650
- tf_anns = view.get_annotations(at_type=AnnotationTypes.TimeFrame)
651
- al_anns = view.get_annotations(at_type=AnnotationTypes.Alignment)
652
-
653
- # Select 'TimeFrame' annotations within given time interval
654
- for tf in tf_anns:
655
- if self._is_in_time_between(start_time, end_time, tf):
656
- valid_tf_anns.append(tf)
657
-
658
- # Map 'TimeFrame' annotation to its aligned annotation
659
- for align in al_anns:
660
- source_id, target_id = align.get_property('source'), align.get_property('target')
661
- to_long_id = lambda x: x if self.id_delimiter in x else f'{view.id}{self.id_delimiter}{x}'
662
- try:
663
- source, target = view.get_annotation_by_id(source_id), view.get_annotation_by_id(target_id)
664
- if source in valid_tf_anns:
665
- tf_to_anns[to_long_id(source_id)].append(target)
666
- elif target in valid_tf_anns:
667
- tf_to_anns[to_long_id(target_id)].append(source)
668
- except KeyError:
669
- pass
670
-
671
- # 3. For those extracted 'TimeFrame' annotations, sort them by their start time
672
- sort_tf_anns = sorted(valid_tf_anns, key=lambda x: self.get_start(x))
673
-
674
- # 4. Yield all annotations aligned with sorted 'TimeFrame' annotations
675
- for tf_ann in sort_tf_anns:
676
- anns = tf_to_anns[tf_ann.long_id]
677
- for ann in anns:
678
- yield ann
710
+
711
+ time_anchors_in_range = []
712
+ at_types = set(at_types)
713
+
714
+ for view in self.get_all_views_contain(AnnotationTypes.TimeFrame) + self.get_all_views_contain(AnnotationTypes.TimePoint):
715
+ time_unit_in_view = view.metadata.contains.get(AnnotationTypes.TimeFrame)["timeUnit"]
716
+
717
+ start_time = convert(start, time_unit, time_unit_in_view, 1)
718
+ end_time = convert(end, time_unit, time_unit_in_view, 1)
719
+ for ann in view.get_annotations():
720
+ if ann.at_type in (AnnotationTypes.TimeFrame, AnnotationTypes.TimePoint) and self._is_in_time_range(ann, start_time, end_time):
721
+ time_anchors_in_range.append(ann)
722
+ time_anchors_in_range.sort(key=lambda x: self.get_start(x))
723
+ for time_anchor in time_anchors_in_range:
724
+ if not at_types or time_anchor.at_type in at_types:
725
+ yield time_anchor
726
+ for aligned in time_anchor.get_all_aligned():
727
+ if not at_types or aligned.at_type in at_types:
728
+ yield aligned
679
729
 
680
730
  def _get_linear_anchor_point(self, ann: Annotation, targets_sorted=False, start: bool = True) -> Union[int, float]:
681
731
  # TODO (krim @ 2/5/24): Update the return type once timeunits are unified to `ms` as integers (https://github.com/clamsproject/mmif/issues/192)
@@ -99,7 +99,7 @@ class MmifObject(object):
99
99
  _exclude_from_diff: Set[str]
100
100
  _attribute_classes: Dict[str, Type] = {} # Mapping: str -> Type
101
101
 
102
- def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None) -> None:
102
+ def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
103
103
  if isinstance(mmif_obj, bytes):
104
104
  mmif_obj = mmif_obj.decode('utf8')
105
105
  if not hasattr(self, '_required_attributes'):
@@ -258,7 +258,7 @@ class MmifObject(object):
258
258
  """
259
259
  for k, v in input_dict.items():
260
260
  if self._attribute_classes and k in self._attribute_classes:
261
- self[k] = self._attribute_classes[k](v)
261
+ self[k] = self._attribute_classes[k](v, self)
262
262
  else:
263
263
  self[k] = v
264
264
 
@@ -285,13 +285,13 @@ class MmifObject(object):
285
285
  if key in self._named_attributes():
286
286
  if self._attribute_classes and key in self._attribute_classes \
287
287
  and not isinstance(value, (self._attribute_classes[key])):
288
- self.__dict__[key] = self._attribute_classes[key](value)
288
+ self.__dict__[key] = self._attribute_classes[key](value, self)
289
289
  else:
290
290
  self.__dict__[key] = value
291
291
  else:
292
292
  if self._attribute_classes and key in self._attribute_classes \
293
293
  and not isinstance(value, (self._attribute_classes[key])):
294
- self.set_additional_property(key, self._attribute_classes[key](value))
294
+ self.set_additional_property(key, self._attribute_classes[key](value, self))
295
295
  else:
296
296
  self.set_additional_property(key, value)
297
297
 
@@ -342,7 +342,7 @@ class DataList(MmifObject, Generic[T]):
342
342
 
343
343
  :param Union[str, list] mmif_obj: the data that the list contains
344
344
  """
345
- def __init__(self, mmif_obj: Optional[Union[bytes, str, list]] = None):
345
+ def __init__(self, mmif_obj: Optional[Union[bytes, str, list]] = None, *_):
346
346
  self.reserved_names.add('_items')
347
347
  self._items: Dict[str, T] = dict()
348
348
  self.disallow_additional_properties()
@@ -440,7 +440,7 @@ class DataList(MmifObject, Generic[T]):
440
440
 
441
441
 
442
442
  class DataDict(MmifObject, Generic[T, S]):
443
- def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None):
443
+ def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None, *_):
444
444
  self.reserved_names.add('_items')
445
445
  self._items: Dict[T, S] = dict()
446
446
  self.disallow_additional_properties()
@@ -9,15 +9,13 @@ import json
9
9
  from datetime import datetime
10
10
  from typing import Dict, Union, Optional, Generator, List, cast
11
11
 
12
- from mmif.serialize.model import PRMTV_TYPES
13
- from mmif.vocabulary import ThingTypesBase, ClamsTypesBase
14
- from .annotation import Annotation, Document
15
- from .model import MmifObject, DataList, DataDict
12
+ from mmif import DocumentTypes, AnnotationTypes, ThingTypesBase, ClamsTypesBase
13
+ from mmif.serialize import model
14
+ from mmif.serialize.annotation import Annotation, Document
15
+ from mmif.serialize.model import PRMTV_TYPES, MmifObject, DataList, DataDict
16
16
 
17
17
  __all__ = ['View', 'ViewMetadata', 'Contain']
18
18
 
19
- from .. import DocumentTypes
20
-
21
19
 
22
20
  class View(MmifObject):
23
21
  """
@@ -32,10 +30,12 @@ class View(MmifObject):
32
30
  :param view_obj: the JSON data that defines the view
33
31
  """
34
32
 
35
- def __init__(self, view_obj: Optional[Union[bytes, str, dict]] = None) -> None:
33
+ def __init__(self, view_obj: Optional[Union[bytes, str, dict]] = None, parent_mmif=None, *_) -> None:
36
34
  # used to autogenerate annotation ids
37
35
  self._id_counts = {}
38
- self.reserved_names.add("_id_counts")
36
+ # used to access the parent MMIF object
37
+ self._parent_mmif = parent_mmif
38
+ self.reserved_names.update(("_parent_mmif", "_id_counts"))
39
39
  self._exclude_from_diff = {"_id_counts"}
40
40
 
41
41
  self.id: str = ''
@@ -64,7 +64,7 @@ class View(MmifObject):
64
64
  else:
65
65
  return self.metadata.new_contain(at_type, **contains_metadata)
66
66
 
67
- def _set_id(self, annotation: Annotation, identifier):
67
+ def _set_ann_id(self, annotation: Annotation, identifier):
68
68
  if identifier is not None:
69
69
  annotation.id = identifier
70
70
  else:
@@ -73,7 +73,7 @@ class View(MmifObject):
73
73
  new_id = f'{prefix}_{new_num}'
74
74
  self._id_counts[prefix] = new_num
75
75
  annotation.id = new_id
76
-
76
+
77
77
  def new_annotation(self, at_type: Union[str, ThingTypesBase], aid: Optional[str] = None,
78
78
  overwrite=False, **properties) -> 'Annotation':
79
79
  """
@@ -96,7 +96,7 @@ class View(MmifObject):
96
96
  """
97
97
  new_annotation = Annotation()
98
98
  new_annotation.at_type = at_type
99
- self._set_id(new_annotation, aid)
99
+ self._set_ann_id(new_annotation, aid)
100
100
  for propk, propv in properties.items():
101
101
  new_annotation.add_property(propk, propv)
102
102
  for propk, propv in self.metadata.contains.get(at_type, {}).items():
@@ -122,8 +122,10 @@ class View(MmifObject):
122
122
  annotation.parent = self.id
123
123
  self.annotations.append(annotation, overwrite)
124
124
  self.new_contain(annotation.at_type)
125
+ if annotation.at_type == AnnotationTypes.Alignment:
126
+ self._parent_mmif._cache_alignment(annotation)
125
127
  return annotation
126
-
128
+
127
129
  def new_textdocument(self, text: str, lang: str = "en", did: Optional[str] = None,
128
130
  overwrite=False, **properties) -> 'Document':
129
131
  """
@@ -147,7 +149,7 @@ class View(MmifObject):
147
149
  """
148
150
  new_document = Document()
149
151
  new_document.at_type = DocumentTypes.TextDocument
150
- self._set_id(new_document, did)
152
+ self._set_ann_id(new_document, did)
151
153
  new_document.text_language = lang
152
154
  new_document.text_value = text
153
155
  for propk, propv in properties.items():
@@ -189,9 +191,18 @@ class View(MmifObject):
189
191
  yield annotation
190
192
 
191
193
  def get_annotation_by_id(self, ann_id) -> Annotation:
192
- ann_found = self.annotations.get(ann_id)
194
+ if self.id_delimiter in ann_id and not ann_id.startswith(self.id):
195
+ try:
196
+ ann_found = self._parent_mmif[ann_id]
197
+ except KeyError:
198
+ ann_found = None
199
+ else:
200
+ ann_found = self.annotations.get(ann_id.split(self.id_delimiter)[-1])
193
201
  if ann_found is None or not isinstance(ann_found, Annotation):
194
- raise KeyError(f"Annotation \"{ann_id}\" is not found in view {self.id}.")
202
+ if self.id_delimiter in ann_id:
203
+ raise KeyError(f"Annotation \"{ann_id}\" is not found in the MMIF.")
204
+ else:
205
+ raise KeyError(f"Annotation \"{ann_id}\" is not found in view {self.id}.")
195
206
  else:
196
207
  return ann_found
197
208
 
@@ -257,7 +268,7 @@ class ViewMetadata(MmifObject):
257
268
  :param viewmetadata_obj: the JSON data that defines the metadata
258
269
  """
259
270
 
260
- def __init__(self, viewmetadata_obj: Optional[Union[bytes, str, dict]] = None) -> None:
271
+ def __init__(self, viewmetadata_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
261
272
  self.document: str = ''
262
273
  self.timestamp: Optional[datetime] = None
263
274
  self.app: str = ''
@@ -377,7 +388,7 @@ class ErrorDict(MmifObject):
377
388
  """
378
389
  Error object that stores information about error occurred during processing.
379
390
  """
380
- def __init__(self, error_obj: Optional[Union[bytes, str, dict]] = None) -> None:
391
+ def __init__(self, error_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
381
392
  self.message: str = ''
382
393
  self.stackTrace: str = ''
383
394
  super().__init__(error_obj)
@@ -429,6 +440,16 @@ class AnnotationsList(DataList[Union[Annotation, Document]]):
429
440
  :return: None
430
441
  """
431
442
  super()._append_with_key(value.id, value, overwrite)
443
+
444
+ def __getitem__(self, key: str):
445
+ """
446
+ specialized getter implementation to workaround https://github.com/clamsproject/mmif/issues/228
447
+ # TODO (krim @ 7/12/24): annotation ids must be in the long form in the future, so this check will be unnecessary once https://github.com/clamsproject/mmif/issues/228 is resolved.
448
+ """
449
+ if ":" in key:
450
+ _, aid = key.split(":")
451
+ return self._items.__getitem__(aid)
452
+ return self._items.get(key, None)
432
453
 
433
454
 
434
455
  class ContainsDict(DataDict[ThingTypesBase, Contain]):
@@ -1,12 +1,12 @@
1
1
  import importlib
2
+ import math
2
3
  import warnings
3
4
  from typing import List, Union, Tuple
4
- import math
5
5
 
6
6
  import mmif
7
7
  from mmif import Annotation, Document, Mmif
8
8
  from mmif.utils.timeunit_helper import convert
9
- from mmif.vocabulary import DocumentTypes, AnnotationTypes
9
+ from mmif.vocabulary import DocumentTypes
10
10
 
11
11
  for cv_dep in ('cv2', 'ffmpeg', 'PIL'):
12
12
  try:
@@ -212,19 +212,19 @@ def convert_timepoint(mmif: Mmif, timepoint: Annotation, out_unit: str) -> Union
212
212
  return convert(timepoint.get_property('timePoint'), in_unit, out_unit, get_framerate(vd))
213
213
 
214
214
 
215
- def convert_timeframe(mmif: Mmif, time_frame: Annotation, out_unit: str) -> Union[Tuple[Union[int, float, str], Union[int, float, str]]]:
215
+ def convert_timeframe(mmif: Mmif, time_frame: Annotation, out_unit: str) -> Tuple[Union[int, float, str], Union[int, float, str]]:
216
216
  """
217
217
  Converts start and end points in a ``TimeFrame`` annotation a different time unit.
218
218
 
219
219
  :param mmif: :py:class:`~mmif.serialize.mmif.Mmif` instance
220
220
  :param time_frame: :py:class:`~mmif.serialize.annotation.Annotation` instance that holds a time interval annotation (``"@type": ".../TimeFrame/..."``)
221
221
  :param out_unit: time unit to which the point is converted
222
- :return: tuple of frame numbers (integer) or seconds/milliseconds (float) of input start and end
222
+ :return: tuple of frame numbers, seconds/milliseconds, or ISO notation of TimeFrame's start and end
223
223
  """
224
224
  in_unit = time_frame.get_property('timeUnit')
225
225
  vd = mmif[time_frame.get_property('document')]
226
- return convert(mmif.get_start(time_frame), in_unit, out_unit, get_framerate(vd)), \
227
- convert(mmif.get_end(time_frame), in_unit, out_unit, get_framerate(vd))
226
+ fps = get_framerate(vd)
227
+ return convert(time_frame.get_property('start'), in_unit, out_unit, fps), convert(time_frame.get_property('end'), in_unit, out_unit, fps)
228
228
 
229
229
 
230
230
  def framenum_to_second(video_doc: Document, frame: int):
@@ -0,0 +1,2 @@
1
+ __version__ = "1.0.18"
2
+ __specver__ = "1.0.5"
@@ -1,4 +1,4 @@
1
- # Spec version 1.0.4
1
+ # Spec version 1.0.5
2
2
  # This file is auto-generated by setup.py
3
3
 
4
4
  from .base_types import AnnotationTypesBase
@@ -7,7 +7,7 @@ from .base_types import AnnotationTypesBase
7
7
  class AnnotationTypes(AnnotationTypesBase):
8
8
  """
9
9
  This class contains the CLAMS annotation types
10
- defined in the spec version 1.0.4 as class variables.
10
+ defined in the spec version 1.0.5 as class variables.
11
11
  """
12
12
 
13
13
  # prpoerty aliases, first added in MMIF 1.0.2
@@ -196,7 +196,7 @@ class DocumentTypesBase(ClamsTypesBase):
196
196
  class ThingType(ThingTypesBase):
197
197
  """
198
198
  This class contains the topmost CLAMS thing type
199
- defined in the spec version 1.0.4 as a class variable.
199
+ defined in the spec version 1.0.5 as a class variable.
200
200
  """
201
201
  Thing = ClamsTypesBase('http://mmif.clams.ai/vocabulary/Thing/v1')
202
202
  _typevers = {'Thing': 'v1'}
@@ -1,4 +1,4 @@
1
- # Spec version 1.0.4
1
+ # Spec version 1.0.5
2
2
  # This file is auto-generated by setup.py
3
3
 
4
4
  from .base_types import DocumentTypesBase
@@ -7,7 +7,7 @@ from .base_types import DocumentTypesBase
7
7
  class DocumentTypes(DocumentTypesBase):
8
8
  """
9
9
  This class contains the CLAMS document types
10
- defined in the spec version 1.0.4 as class variables.
10
+ defined in the spec version 1.0.5 as class variables.
11
11
  """
12
12
  Document = DocumentTypesBase('http://mmif.clams.ai/vocabulary/Document/v1')
13
13
  VideoDocument = DocumentTypesBase('http://mmif.clams.ai/vocabulary/VideoDocument/v1')
@@ -10,3 +10,7 @@ def resolve(docloc):
10
10
  raise ValueError(f'cannot handle document location scheme: {docloc}')
11
11
  except urllib.error.URLError as e:
12
12
  raise e
13
+
14
+
15
+ def help():
16
+ return "location must be a URL string."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mmif-python
3
- Version: 1.0.16
3
+ Version: 1.0.18
4
4
  Summary: Python implementation of MultiMedia Interchange Format specification. (https://mmif.clams.ai)
5
5
  Home-page: https://mmif.clams.ai
6
6
  Author: Brandeis Lab for Linguistics and Computation
@@ -222,7 +222,7 @@ class TestMmif(unittest.TestCase):
222
222
  def test_document_location_helpers_http(self):
223
223
  new_doc = Document()
224
224
  new_doc.id = "d1"
225
- new_doc.location = f"https://www.gnu.org/licenses/gpl-3.0.txt"
225
+ new_doc.location = f"https://example.com/"
226
226
  self.assertEqual(new_doc.location_scheme(), 'https')
227
227
  try:
228
228
  path = new_doc.location_path()
@@ -277,11 +277,11 @@ class TestMmif(unittest.TestCase):
277
277
  self.assertEqual(2, len(views))
278
278
  views = mmif_obj.get_all_views_contain('http://vocab.lappsgrid.org/SemanticTag')
279
279
  self.assertEqual(1, len(views))
280
- views = mmif_obj.get_views_contain([
280
+ views = mmif_obj.get_views_contain(
281
281
  AnnotationTypes.TimeFrame,
282
282
  DocumentTypes.TextDocument,
283
283
  AnnotationTypes.Alignment,
284
- ])
284
+ )
285
285
  self.assertEqual(1, len(views))
286
286
  views = mmif_obj.get_all_views_contain(not_existing_attype)
287
287
  self.assertEqual(0, len(views))
@@ -323,6 +323,17 @@ class TestMmif(unittest.TestCase):
323
323
  views_and_alignments = mmif_obj.get_alignments(DocumentTypes.TextDocument, AnnotationTypes.BoundingBox)
324
324
  self.assertEqual(1, len(views_and_alignments))
325
325
  self.assertTrue('v6' in views_and_alignments)
326
+
327
+ def test_cache_alignment(self):
328
+ mmif_obj = Mmif(MMIF_EXAMPLES['everything'])
329
+ views_and_alignments = mmif_obj.get_alignments(DocumentTypes.TextDocument, AnnotationTypes.TimeFrame)
330
+ for vid, alignments in views_and_alignments.items():
331
+ v = mmif_obj.get_view_by_id(vid)
332
+ for alignment in alignments:
333
+ s = v.get_annotation_by_id(alignment.get('source'))
334
+ t = v.get_annotation_by_id(alignment.get('target'))
335
+ self.assertTrue(s.aligned_to_by(alignment).long_id.endswith(t.long_id))
336
+ self.assertTrue(t.aligned_to_by(alignment).long_id.endswith(s.long_id))
326
337
 
327
338
  def test_new_view_id(self):
328
339
  p = Mmif.view_prefix
@@ -350,7 +361,7 @@ class TestMmif(unittest.TestCase):
350
361
  # .[] |
351
362
  # select(."@type"=="http://vocab.lappsgrid.org/Token")] |
352
363
  # sort_by(.properties.id | ltrimstr("t") | tonumber) |
353
- # map(.properties.text)' <examples>.json
364
+ # map(.properties.word)' <examples>.json
354
365
  tokens_in_order = ["Hello",
355
366
  ",",
356
367
  "this",
@@ -382,23 +393,26 @@ class TestMmif(unittest.TestCase):
382
393
  mmif_obj = Mmif(MMIF_EXAMPLES['everything'])
383
394
 
384
395
  # Test case 1: All token annotations are selected
385
- selected_token_anns = mmif_obj.get_annotations_between_time(0, 22000)
386
- self.assertEqual(28, len(list(selected_token_anns)))
396
+ selected_token_anns = [ann for ann in mmif_obj.get_annotations_between_time(0, 22000) if ann.is_type(token_type)]
397
+ self.assertEqual(28, len(selected_token_anns))
387
398
  for i, ann in enumerate(selected_token_anns):
388
- self.assertTrue(ann.is_type(token_type))
389
- self.assertEqual(tokens_in_order[i], ann.get_property("text"))
399
+ self.assertEqual(tokens_in_order[i], ann.get_property("word"))
390
400
 
391
401
  # Test case 2: No token annotation are selected
392
- selected_token_anns = mmif_obj.get_annotations_between_time(0, 5, time_unit="seconds")
393
- self.assertEqual(0, len(list(selected_token_anns)))
402
+ selected_token_anns = list(mmif_obj.get_annotations_between_time(0, 5, time_unit="seconds"))
403
+ self.assertEqual(4, len(list(selected_token_anns)))
404
+ for ann in selected_token_anns:
405
+ self.assertFalse(ann.is_type(token_type))
394
406
 
395
407
  # Test case 3(a): Partial tokens are selected (involve partial overlap)
396
- selected_token_anns = mmif_obj.get_annotations_between_time(7, 10, time_unit="seconds")
397
- self.assertEqual(tokens_in_order[3:9], [ann.get_property("text") for ann in selected_token_anns])
408
+ selected_token_anns = mmif_obj.get_annotations_between_time(7, 10, time_unit="seconds",
409
+ at_types=['http://vocab.lappsgrid.org/Token'])
410
+ self.assertEqual(tokens_in_order[3:9], [ann.get_property("word") for ann in selected_token_anns])
398
411
 
399
412
  # Test case 3(b): Partial tokens are selected (only full overlap)
400
- selected_token_anns = mmif_obj.get_annotations_between_time(11500, 14600)
401
- self.assertEqual(tokens_in_order[12:17], [ann.get_property("text") for ann in selected_token_anns])
413
+ selected_token_anns = mmif_obj.get_annotations_between_time(11500, 14600,
414
+ at_types=['http://vocab.lappsgrid.org/Token'])
415
+ self.assertEqual(tokens_in_order[12:17], [ann.get_property("word") for ann in selected_token_anns])
402
416
 
403
417
  def test_add_document(self):
404
418
  mmif_obj = Mmif(MMIF_EXAMPLES['everything'])
@@ -662,6 +676,7 @@ class TestView(unittest.TestCase):
662
676
  self.assertEqual(view_from_str, view_from_bytes)
663
677
  self.assertEqual(json.loads(view_from_json.serialize()), json.loads(view_from_str.serialize()))
664
678
  self.assertEqual(json.loads(view_from_bytes.serialize()), json.loads(view_from_str.serialize()))
679
+
665
680
 
666
681
  def test_annotation_order_preserved(self):
667
682
  view_serial = self.view_obj.serialize()
@@ -885,13 +900,12 @@ class TestAnnotation(unittest.TestCase):
885
900
  # TODO (angus-lherrou @ 7/27/2020): testing should include validation for required attrs
886
901
  # once that is implemented (issue #23)
887
902
  def setUp(self) -> None:
888
- self.data = {i: {'string': example,
889
- 'json': json.loads(example),
890
- 'mmif': Mmif(example),
891
- 'annotations': [annotation
892
- for view in json.loads(example)['views']
893
- for annotation in view['annotations']]}
894
- for i, example in MMIF_EXAMPLES.items()}
903
+ self.data = {}
904
+ for i, example in MMIF_EXAMPLES.items():
905
+ self.data[i] = {'string': example,
906
+ 'json': json.loads(example),
907
+ 'mmif': Mmif(example),
908
+ 'annotations': [annotation for view in json.loads(example)['views'] for annotation in view['annotations']]}
895
909
 
896
910
  def test_annotation_properties(self):
897
911
  ann_json = self.data['everything']['annotations'][0]
@@ -1104,11 +1118,11 @@ class TestDocument(unittest.TestCase):
1104
1118
  # then converted to an `Annotation` annotation during serialization at `Mmif`-level
1105
1119
  mmif.add_document(doc1)
1106
1120
  ## no Annotation before serialization
1107
- self.assertEqual(0, len(list(mmif.views.get_last().get_annotations(AnnotationTypes.Annotation, author='me'))))
1121
+ self.assertEqual(0, len(list(mmif.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation, author='me'))))
1108
1122
  ## after serialization, the `Annotation` annotation should be added to the last view
1109
1123
  ## note that we didn't add any views, so the last view before and after the serialization are the same
1110
1124
  mmif_roundtrip = Mmif(mmif.serialize())
1111
- self.assertTrue(next(mmif_roundtrip.views.get_last().get_annotations(AnnotationTypes.Annotation, author='me')))
1125
+ self.assertTrue(next(mmif_roundtrip.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation, author='me')))
1112
1126
  # finally, when deserialized back to a Mmif instance, the `Annotation` props should be added
1113
1127
  # as a property of the document
1114
1128
  doc1_mmif_roundtrip = mmif_roundtrip.get_document_by_id('doc1')
@@ -1121,7 +1135,7 @@ class TestDocument(unittest.TestCase):
1121
1135
  mmif.add_document(doc1)
1122
1136
  did = 'doc1'
1123
1137
  # last view before serialization rounds
1124
- r0_vid = mmif.views.get_last().id
1138
+ r0_vid = mmif.views.get_last_contentful_view().id
1125
1139
  doc1.at_type = DocumentTypes.TextDocument
1126
1140
  doc1.id = did
1127
1141
  doc1.location = 'aScheme:///data/doc1.txt'
@@ -1151,7 +1165,7 @@ class TestDocument(unittest.TestCase):
1151
1165
  self.assertEqual(2, len(doc1._props_pending))
1152
1166
  mmif_roundtrip2 = Mmif(mmif_roundtrip1.serialize())
1153
1167
  ## but not serialized
1154
- self.assertEqual(0, len(list(mmif_roundtrip2.views.get_last().get_annotations(AnnotationTypes.Annotation))))
1168
+ self.assertEqual(0, len(list(mmif_roundtrip2.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))))
1155
1169
 
1156
1170
  # adding non-duplicate value should be serialized into a new Annotation object
1157
1171
  # even when there is a duplicate key in a previous view
@@ -1197,7 +1211,7 @@ class TestDocument(unittest.TestCase):
1197
1211
  doc1.add_property('author', 'me')
1198
1212
  mmif_roundtrip = Mmif(mmif.serialize())
1199
1213
  ## should be only one Annotation object, from manual call of `new_annotation`
1200
- self.assertEqual(1, len(list(mmif_roundtrip.views.get_last().get_annotations(AnnotationTypes.Annotation))))
1214
+ self.assertEqual(1, len(list(mmif_roundtrip.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))))
1201
1215
 
1202
1216
  # test with duplicate property added by a downstream app
1203
1217
  mmif_roundtrip = Mmif(mmif.serialize())
@@ -1209,9 +1223,9 @@ class TestDocument(unittest.TestCase):
1209
1223
  # author=me is already in the input MMIF
1210
1224
  doc1_prime.add_property('author', 'me')
1211
1225
  mmif_roundtrip2 = Mmif(mmif_roundtrip.serialize())
1212
- self.assertEqual(1, len(list(mmif.views.get_last().get_annotations(AnnotationTypes.Annotation))))
1226
+ self.assertEqual(1, len(list(mmif.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))))
1213
1227
  # none should be added
1214
- self.assertEqual(0, len(list(mmif_roundtrip2.views.get_last().get_annotations(AnnotationTypes.Annotation))))
1228
+ self.assertEqual(0, len(list(mmif_roundtrip2.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))))
1215
1229
 
1216
1230
  # test with same key, different value
1217
1231
  mmif_roundtrip = Mmif(mmif.serialize())
@@ -1224,13 +1238,13 @@ class TestDocument(unittest.TestCase):
1224
1238
  doc1_prime.add_property('author', 'you')
1225
1239
  mmif_roundtrip2 = Mmif(mmif_roundtrip.serialize())
1226
1240
  # the Annotation in the previous view should be preserved
1227
- self.assertEqual(1, len(list(mmif.views.get_last().get_annotations(AnnotationTypes.Annotation))))
1241
+ self.assertEqual(1, len(list(mmif.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))))
1228
1242
  # but a new one should be added
1229
- self.assertEqual(1, len(list(mmif_roundtrip2.views.get_last().get_annotations(AnnotationTypes.Annotation))))
1243
+ self.assertEqual(1, len(list(mmif_roundtrip2.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))))
1230
1244
 
1231
- self.assertEqual('me', list(mmif.views.get_last().get_annotations(AnnotationTypes.Annotation))[0].get_property(
1245
+ self.assertEqual('me', list(mmif.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))[0].get_property(
1232
1246
  'author'))
1233
- self.assertEqual('you', list(mmif_roundtrip2.views.get_last().get_annotations(AnnotationTypes.Annotation))[
1247
+ self.assertEqual('you', list(mmif_roundtrip2.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))[
1234
1248
  0].get_property('author'))
1235
1249
 
1236
1250
  def test_capital_annotation_generation_viewfinder(self):
@@ -1255,6 +1269,28 @@ class TestDocument(unittest.TestCase):
1255
1269
  cap_anns = list(mmif_roundtrip.views[f'v{i}'].get_annotations(AnnotationTypes.Annotation))
1256
1270
  self.assertEqual(1, len(cap_anns))
1257
1271
  self.assertEqual(authors[i-1], cap_anns[0].get_property('author'))
1272
+
1273
+ def test_capital_annotation_nongeneration_for_writable_documents(self):
1274
+ mmif = Mmif(validate=False)
1275
+ doc = Document()
1276
+ doc.at_type = DocumentTypes.TextDocument
1277
+ doc.id = f'doc0'
1278
+ doc.location = f'aScheme:///data/doc0.txt'
1279
+ mmif.add_document(doc)
1280
+
1281
+ v = mmif.new_view()
1282
+ v.metadata.app = tester_appname
1283
+ vid = v.id
1284
+ new_td_id = mmif[vid].new_textdocument(text='new text', document='doc0', origin='transformation').id
1285
+ doc.add_property('author', 'me')
1286
+
1287
+ mmif_roundtrip = Mmif(mmif.serialize())
1288
+
1289
+ self.assertTrue(AnnotationTypes.Annotation in mmif_roundtrip[vid].metadata.contains)
1290
+ self.assertTrue(mmif_roundtrip.get_document_by_id('doc0').get_property('author'), 'me')
1291
+ self.assertTrue(next(mmif_roundtrip[vid].get_annotations(AnnotationTypes.Annotation)).get_property('author'), 'me')
1292
+ self.assertTrue(next(mmif_roundtrip[vid].get_annotations(AnnotationTypes.Annotation)).get_property('document'), doc.id)
1293
+ self.assertTrue(mmif_roundtrip[new_td_id].get_property('origin'), 'transformation')
1258
1294
 
1259
1295
  def test_deserialize_with_whole_mmif(self):
1260
1296
  for i, datum in self.data.items():
@@ -4,10 +4,9 @@ import pytest
4
4
 
5
5
  from mmif import Mmif, Document, AnnotationTypes
6
6
  from mmif.utils import sequence_helper as sqh
7
+ from mmif.utils import text_document_helper as tdh
7
8
  from mmif.utils import timeunit_helper as tuh
8
9
  from mmif.utils import video_document_helper as vdh
9
- from mmif.utils import text_document_helper as tdh
10
- from mmif.serialize import mmif
11
10
  from tests.mmif_examples import *
12
11
 
13
12
 
@@ -178,7 +177,7 @@ class TestSequenceHelper(unittest.TestCase):
178
177
  class TestTextDocHelper(unittest.TestCase):
179
178
  mmif_obj = Mmif(MMIF_EXAMPLES['everything'])
180
179
 
181
- @pytest.mark.skip("The only valid test cases come from kalbi app which annotates wrong property")
180
+ @pytest.mark.skip("The only valid test cases come from kaldi app which annotates wrong property")
182
181
  def test_slice_text(self):
183
182
  sliced_text_full_overlap = tdh.slice_text(self.mmif_obj, 11500, 14600)
184
183
  sliced_text_partial_overlap = tdh.slice_text(self.mmif_obj, 7, 10, unit="seconds")
@@ -1 +0,0 @@
1
- 1.0.16
@@ -1,2 +0,0 @@
1
- __version__ = "1.0.16"
2
- __specver__ = "1.0.4"
File without changes
File without changes
File without changes
File without changes
File without changes