mmif-python 1.0.11__tar.gz → 1.0.15__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 (45) hide show
  1. mmif_python-1.0.15/PKG-INFO +46 -0
  2. mmif_python-1.0.15/VERSION +1 -0
  3. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/serialize/annotation.py +34 -15
  4. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/serialize/mmif.py +160 -129
  5. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/serialize/model.py +26 -19
  6. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/serialize/view.py +42 -4
  7. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/utils/timeunit_helper.py +4 -4
  8. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/utils/video_document_helper.py +50 -10
  9. mmif_python-1.0.15/mmif/ver/__init__.py +2 -0
  10. mmif_python-1.0.15/mmif_python.egg-info/PKG-INFO +46 -0
  11. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif_python.egg-info/SOURCES.txt +5 -1
  12. mmif_python-1.0.15/tests/test_serialize.py +1354 -0
  13. mmif_python-1.0.15/tests/test_utils.py +176 -0
  14. mmif_python-1.0.15/tests/test_versioncompat.py +56 -0
  15. mmif_python-1.0.15/tests/test_vocab.py +56 -0
  16. mmif-python-1.0.11/PKG-INFO +0 -37
  17. mmif-python-1.0.11/VERSION +0 -1
  18. mmif-python-1.0.11/mmif/ver/__init__.py +0 -2
  19. mmif-python-1.0.11/mmif_python.egg-info/PKG-INFO +0 -37
  20. {mmif-python-1.0.11 → mmif_python-1.0.15}/LICENSE +0 -0
  21. {mmif-python-1.0.11 → mmif_python-1.0.15}/MANIFEST.in +0 -0
  22. {mmif-python-1.0.11 → mmif_python-1.0.15}/README.md +0 -0
  23. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/__init__.py +0 -0
  24. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/res/__init__.py +0 -0
  25. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/res/clams.vocabulary.yaml +0 -0
  26. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/res/do-not-edit.txt +0 -0
  27. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/res/mmif.json +0 -0
  28. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/serialize/__init__.py +0 -0
  29. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/utils/__init__.py +0 -0
  30. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/utils/sequence_helper.py +0 -0
  31. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/ver/do-not-edit.txt +0 -0
  32. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/vocabulary/__init__.py +0 -0
  33. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/vocabulary/annotation_types.py +0 -0
  34. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/vocabulary/base_types.py +0 -0
  35. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/vocabulary/do-not-edit.txt +0 -0
  36. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/vocabulary/document_types.py +0 -0
  37. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif_docloc_http/__init__.py +0 -0
  38. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif_python.egg-info/dependency_links.txt +0 -0
  39. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif_python.egg-info/requires.txt +0 -0
  40. {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif_python.egg-info/top_level.txt +0 -0
  41. {mmif-python-1.0.11 → mmif_python-1.0.15}/requirements.cv +0 -0
  42. {mmif-python-1.0.11 → mmif_python-1.0.15}/requirements.seq +0 -0
  43. {mmif-python-1.0.11 → mmif_python-1.0.15}/requirements.txt +0 -0
  44. {mmif-python-1.0.11 → mmif_python-1.0.15}/setup.cfg +0 -0
  45. {mmif-python-1.0.11 → mmif_python-1.0.15}/setup.py +0 -0
@@ -0,0 +1,46 @@
1
+ Metadata-Version: 2.1
2
+ Name: mmif-python
3
+ Version: 1.0.15
4
+ Summary: Python implementation of MultiMedia Interchange Format specification. (https://mmif.clams.ai)
5
+ Home-page: https://mmif.clams.ai
6
+ Author: Brandeis Lab for Linguistics and Computation
7
+ Author-email: admin@clams.ai
8
+ Classifier: Development Status :: 2 - Pre-Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: Apache Software License
11
+ Classifier: Programming Language :: Python :: 3 :: Only
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: deepdiff>5
16
+ Requires-Dist: jsonschema
17
+ Provides-Extra: seq
18
+ Requires-Dist: numpy; extra == "seq"
19
+ Provides-Extra: cv
20
+ Requires-Dist: pillow; extra == "cv"
21
+ Requires-Dist: opencv-python; extra == "cv"
22
+ Requires-Dist: ffmpeg-python; extra == "cv"
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest; extra == "dev"
25
+ Requires-Dist: pytest-pep8; extra == "dev"
26
+ Requires-Dist: pytest-cov; extra == "dev"
27
+ Requires-Dist: pytype; extra == "dev"
28
+
29
+ ## MultiMedia Interchange Format
30
+ [MMIF](https://mmif.clams.ai) is a JSON(-LD)-based data format designed for transferring annotation data between computational analysis applications in [CLAMS project](https://clams.ai).
31
+
32
+
33
+ ## mmif-python
34
+ `mmif-python` is a Python implementation of the MMIF data format.
35
+ `mmif-python` provides various helper classes and functions to handle MMIF JSON in Python,
36
+ including ;
37
+
38
+ 1. de-/serialization of MMIF internal data structures to/from JSON
39
+ 2. validation of MMIF JSON
40
+ 3. handling of CLAMS vocabulary types
41
+ 4. navigation of MMIF object via various "search" methods (e.g. `mmif.get_all_views_contain(vocab_type))`)
42
+
43
+ ## For more ...
44
+ * [Version history and patch notes](https://github.com/clamsproject/mmif-python/blob/main/CHANGELOG.md)
45
+ * [MMIF Python API documentation](https://clamsproject.github.io/mmif-python)
46
+ * [MMIF JSON specification and schema](https://clamsproject.github.io/mmif)
@@ -0,0 +1 @@
1
+ 1.0.15
@@ -26,7 +26,7 @@ __all__ = ['Annotation', 'AnnotationProperties', 'Document', 'DocumentProperties
26
26
 
27
27
  T = TypeVar('T')
28
28
  LIST_PRMTV = typing.List[PRMTV_TYPES] # list of values (most cases for annotation props)
29
- LIST_LIST_PRMTV = typing.List[LIST_PRMTV] # list of list of values (e.g. for coordinates)
29
+ LIST_LIST_PRMTV = typing.List[LIST_PRMTV] # list of lists of values (e.g. for coordinates)
30
30
  DICT_PRMTV = typing.Dict[str, PRMTV_TYPES] # dict of values (`text` prop of `TextDocument` and other complex props)
31
31
  DICT_LIST_PRMTV = typing.Dict[str, LIST_PRMTV] # dict of list of values (even more complex props)
32
32
 
@@ -127,6 +127,20 @@ class Annotation(MmifObject):
127
127
  def id(self, aid: str) -> None:
128
128
  self.properties.id = aid
129
129
 
130
+ @property
131
+ def long_id(self) -> str:
132
+ if self.parent is not None and len(self.parent) > 0:
133
+ return f"{self.parent}{self.id_delimiter}{self.id}"
134
+ else:
135
+ return self.id
136
+
137
+ @long_id.setter
138
+ def long_id(self, long_id: str) -> None:
139
+ if self.id_delimiter in long_id:
140
+ self.parent, self.id = long_id.split(self.id_delimiter)
141
+ else:
142
+ self.id = long_id
143
+
130
144
  @staticmethod
131
145
  def check_prop_value_is_simple_enough(
132
146
  value: Union[PRMTV_TYPES, LIST_PRMTV, LIST_LIST_PRMTV, DICT_PRMTV, DICT_LIST_PRMTV]) -> bool:
@@ -401,26 +415,31 @@ class AnnotationProperties(MmifObject, MutableMapping[str, T]):
401
415
 
402
416
  def __iter__(self) -> Iterator[str]:
403
417
  """
404
- ``__iter__`` on Mapping should basically work as ``keys()`` method of vanilla dict
405
- however, when MMIF objects are serialized, all optional (not in ``_req_atts``),
406
- empty props are ignored (note that emtpy but required props are serialized
407
- with the *emtpy* value).
408
- Hence, this ``__iter__`` method should also work in the same way and
409
- ignore empty optional props.
418
+ ``__iter__`` on Mapping should basically work as ``keys()`` method
419
+ of vanilla dict.
410
420
  """
411
421
  for key in itertools.chain(self._named_attributes(), self._unnamed_attributes):
412
- if key in self._required_attributes:
413
- yield key
414
- else:
415
- try:
416
- self.__getitem__(key)
417
- yield key
418
- except KeyError:
419
- pass
422
+ yield key
423
+
424
+ def __getitem__(self, key):
425
+ """
426
+ Parent MmifObject class has a __getitem__ method that checks if
427
+ the value is empty when asked for an unnamed attribute. But for
428
+ AnnotationProperties, any arbitrary property that's added
429
+ explicitly by the user (developer) should not be ignored and
430
+ returned even the value is empty.
431
+ """
432
+ if key in self._named_attributes():
433
+ return self.__dict__[key]
434
+ else:
435
+ return self._unnamed_attributes[key]
420
436
 
421
437
  def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None) -> None:
422
438
  self.id: str = ''
439
+ # any individual at_type (subclassing this class) can have its own set of required attributes
423
440
  self._required_attributes = ["id"]
441
+ # allowing additional attributes for arbitrary annotation properties
442
+ self._unnamed_attributes = {}
424
443
  super().__init__(mmif_obj)
425
444
 
426
445
 
@@ -10,13 +10,13 @@ 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, ClassVar, cast
13
+ from typing import List, Union, Optional, Dict, cast
14
14
 
15
15
  import jsonschema.validators
16
+
16
17
  import mmif
17
18
  from mmif import ThingTypesBase
18
19
  from mmif.vocabulary import AnnotationTypes, DocumentTypes
19
-
20
20
  from .annotation import Annotation, Document
21
21
  from .model import MmifObject, DataList
22
22
  from .view import View
@@ -24,6 +24,104 @@ from .view import View
24
24
  __all__ = ['Mmif']
25
25
 
26
26
 
27
+ class MmifMetadata(MmifObject):
28
+ """
29
+ Basic MmifObject class to contain the top-level metadata of a MMIF file.
30
+
31
+ :param metadata_obj: the JSON data
32
+ """
33
+
34
+ def __init__(self, metadata_obj: Optional[Union[bytes, str, dict]] = None) -> None:
35
+ # TODO (krim @ 10/7/20): there could be a better name and a better way to give a value to this
36
+ self.mmif: str = f"http://mmif.clams.ai/{mmif.__specver__}"
37
+ self._required_attributes = ["mmif"]
38
+ super().__init__(metadata_obj)
39
+
40
+
41
+ class DocumentsList(DataList[Document]):
42
+ """
43
+ DocumentsList object that implements :class:`mmif.serialize.model.DataList`
44
+ for :class:`mmif.serialize.document.Document`.
45
+ """
46
+ _items: Dict[str, Document]
47
+
48
+ def _deserialize(self, input_list: list) -> None: # pytype: disable=signature-mismatch
49
+ """
50
+ Extends base ``_deserialize`` method to initialize ``items`` as a dict from
51
+ document IDs to :class:`mmif.serialize.document.Document` objects.
52
+
53
+ :param input_list: the JSON data that defines the list of documents
54
+ :return: None
55
+ """
56
+ self._items = {item['properties']['id']: Document(item) for item in input_list}
57
+
58
+ def append(self, value: Document, overwrite=False) -> None:
59
+ """
60
+ Appends a document to the list.
61
+
62
+ Fails if there is already a document with the same ID
63
+ in the list, unless ``overwrite`` is set to True.
64
+
65
+ :param value: the :class:`mmif.serialize.document.Document`
66
+ object to add
67
+ :param overwrite: if set to True, will overwrite an
68
+ existing document with the same ID
69
+ :raises KeyError: if ``overwrite`` is set to False and
70
+ a document with the same ID exists
71
+ in the list
72
+ :return: None
73
+ """
74
+ super()._append_with_key(value.id, value, overwrite)
75
+
76
+
77
+ class ViewsList(DataList[View]):
78
+ """
79
+ ViewsList object that implements :class:`mmif.serialize.model.DataList`
80
+ for :class:`mmif.serialize.view.View`.
81
+ """
82
+ _items: Dict[str, View]
83
+
84
+ def __init__(self, mmif_obj: Optional[Union[bytes, str, list]] = None):
85
+ super().__init__(mmif_obj)
86
+
87
+ def _deserialize(self, input_list: list) -> None: # pytype: disable=signature-mismatch
88
+ """
89
+ Extends base ``_deserialize`` method to initialize ``items`` as a dict from
90
+ view IDs to :class:`mmif.serialize.view.View` objects.
91
+
92
+ :param input_list: the JSON data that defines the list of views
93
+ :return: None
94
+ """
95
+ if input_list:
96
+ self._items = {item['id']: View(item) for item in input_list}
97
+
98
+ def append(self, value: View, overwrite=False) -> None:
99
+ """
100
+ Appends a view to the list.
101
+
102
+ Fails if there is already a view with the same ID
103
+ in the list, unless ``overwrite`` is set to True.
104
+
105
+ :param value: the :class:`mmif.serialize.view.View`
106
+ object to add
107
+ :param overwrite: if set to True, will overwrite an
108
+ existing view with the same ID
109
+ :raises KeyError: if ``overwrite`` is set to False and
110
+ a view with the same ID exists
111
+ in the list
112
+ :return: None
113
+ """
114
+ super()._append_with_key(value.id, value, overwrite)
115
+
116
+ def get_last(self) -> Optional[View]:
117
+ """
118
+ Returns the last view appended to the list.
119
+ """
120
+ for view in reversed(self._items.values()):
121
+ if 'error' not in view.metadata and 'warning' not in view.metadata:
122
+ return view
123
+
124
+
27
125
  class Mmif(MmifObject):
28
126
  """
29
127
  MmifObject that represents a full MMIF file.
@@ -32,9 +130,6 @@ class Mmif(MmifObject):
32
130
  :param validate: whether to validate the data against the MMIF JSON schema.
33
131
  """
34
132
 
35
- view_prefix: ClassVar[str] = 'v_'
36
- id_delimiter: ClassVar[str] = ':'
37
-
38
133
  def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None, *, validate: bool = True) -> None:
39
134
  self.metadata: MmifMetadata = MmifMetadata()
40
135
  self.documents: DocumentsList = DocumentsList()
@@ -362,8 +457,8 @@ class Mmif(MmifObject):
362
457
  :return: a reference to the corresponding document, if it exists
363
458
  :raises KeyError: if there is no corresponding document
364
459
  """
365
- if Mmif.id_delimiter in doc_id:
366
- vid, did = doc_id.split(Mmif.id_delimiter)
460
+ if self.id_delimiter in doc_id:
461
+ vid, did = doc_id.split(self.id_delimiter)
367
462
  view = self[vid]
368
463
  if isinstance(view, View):
369
464
  return view.get_document_by_id(did)
@@ -410,8 +505,8 @@ class Mmif(MmifObject):
410
505
  aligned_types = set()
411
506
  for ann_id in [alignment['target'], alignment['source']]:
412
507
  ann_id = cast(str, ann_id)
413
- if Mmif.id_delimiter in ann_id:
414
- view_id, ann_id = ann_id.split(Mmif.id_delimiter)
508
+ if self.id_delimiter in ann_id:
509
+ view_id, ann_id = ann_id.split(self.id_delimiter)
415
510
  aligned_type = cast(Annotation, self[view_id][ann_id]).at_type
416
511
  else:
417
512
  aligned_type = cast(Annotation, alignment_view[ann_id]).at_type
@@ -438,8 +533,8 @@ class Mmif(MmifObject):
438
533
  except StopIteration:
439
534
  # means search failed by the full doc_id string,
440
535
  # now try trimming the view_id from the string and re-do the search
441
- if Mmif.id_delimiter in doc_id:
442
- vid, did = doc_id.split(Mmif.id_delimiter)
536
+ if self.id_delimiter in doc_id:
537
+ vid, did = doc_id.split(self.id_delimiter)
443
538
  if view.id == vid:
444
539
  annotations = view.get_annotations(document=did)
445
540
  try:
@@ -450,6 +545,16 @@ class Mmif(MmifObject):
450
545
  pass
451
546
  return views
452
547
 
548
+ def get_all_views_with_error(self) -> List[View]:
549
+ """
550
+ Returns the list of all views in the MMIF that have errors.
551
+
552
+ :return: the list of views that contain errors but no annotations
553
+ """
554
+ return [v for v in self.views if v.has_error()]
555
+
556
+ get_views_with_error = get_all_views_with_error
557
+
453
558
  def get_all_views_contain(self, at_types: Union[ThingTypesBase, str, List[Union[str, ThingTypesBase]]]) -> List[View]:
454
559
  """
455
560
  Returns the list of all views in the MMIF if given types
@@ -464,11 +569,27 @@ class Mmif(MmifObject):
464
569
  else:
465
570
  return [view for view in self.views if at_types in view.metadata.contains]
466
571
 
467
- def get_views_contain(self, at_types: Union[ThingTypesBase, str, List[Union[str, ThingTypesBase]]]) -> List[View]:
572
+ get_views_contain = get_all_views_contain
573
+
574
+ def get_view_with_error(self) -> Optional[View]:
468
575
  """
469
- An alias to `get_all_views_contain` method.
576
+ Returns the last view appended that contains an error.
577
+
578
+ :return: the view, or None if no error is found
470
579
  """
471
- return self.get_all_views_contain(at_types)
580
+ for view in reversed(self.views):
581
+ if view.has_error():
582
+ return view
583
+ return None
584
+
585
+ def get_last_error(self) -> Optional[str]:
586
+ """
587
+ Returns the last error message found in the views.
588
+
589
+ :return: the error message in human-readable format, or None if no error is found
590
+ """
591
+ v = self.get_view_with_error()
592
+ return v.get_error() if v is not None else None
472
593
 
473
594
  def get_view_contains(self, at_types: Union[ThingTypesBase, str, List[Union[str, ThingTypesBase]]]) -> Optional[View]:
474
595
  """
@@ -506,8 +627,8 @@ class Mmif(MmifObject):
506
627
  elif 'targets' in props:
507
628
 
508
629
  def get_target_ann(cur_ann, target_id):
509
- if Mmif.id_delimiter not in target_id:
510
- target_id = Mmif.id_delimiter.join((cur_ann.parent, target_id))
630
+ if self.id_delimiter not in target_id:
631
+ target_id = self.id_delimiter.join((cur_ann.parent, target_id))
511
632
  return self.__getitem__(target_id)
512
633
 
513
634
  if not targets_sorted:
@@ -537,131 +658,41 @@ class Mmif(MmifObject):
537
658
  """
538
659
  return self._get_linear_anchor_point(annotation, start=False)
539
660
 
540
- # pytype: disable=bad-return-type
541
- def __getitem__(self, item: str) -> Union[Document, View, Annotation]:
661
+ def __getitem__(self, item: str) \
662
+ -> Union[Document, View, Annotation, MmifMetadata, DocumentsList, ViewsList]:
542
663
  """
543
- getitem implementation for Mmif. When nothing is found, this will raise an error
544
- rather than returning a None (although pytype doesn't think so...)
664
+ getitem implementation for Mmif. This will try to find any object, given an identifier or an immediate
665
+ attribute name. When nothing is found, this will raise an error rather than returning a None
545
666
 
546
667
  :raises KeyError: if the item is not found or if the search results are ambiguous
547
- :param item: the search string, a document ID, a view ID, or a view-scoped annotation ID
668
+ :param item: an attribute name or an object identifier (a document ID, a view ID, or an annotation ID). When
669
+ annotation ID is given as a "short" ID (without view ID prefix), the method will try to find a
670
+ match from the first view, and return immediately if found.
548
671
  :return: the object searched for
672
+ :raise KeyError: if the item is not found or multiple objects are found with the same ID
549
673
  """
550
674
  if item in self._named_attributes():
551
675
  return self.__dict__[item]
552
- split_attempt = item.split(Mmif.id_delimiter)
676
+ split_attempt = item.split(self.id_delimiter)
553
677
 
554
- document_result = self.documents.get(split_attempt[0])
555
- view_result = self.views.get(split_attempt[0])
678
+ found = []
556
679
 
557
680
  if len(split_attempt) == 1:
558
- anno_result = None
559
- elif view_result:
560
- anno_result = view_result[split_attempt[1]]
681
+ found.append(self.documents.get(split_attempt[0]))
682
+ found.append(self.views.get(split_attempt[0]))
683
+ for view in self.views:
684
+ found.append(view.annotations.get(split_attempt[0]))
685
+ elif len(split_attempt) == 2:
686
+ v = self.get_view_by_id(split_attempt[0])
687
+ if v is not None:
688
+ found.append(v.annotations.get(split_attempt[1]))
561
689
  else:
562
690
  raise KeyError("Tried to subscript into a view that doesn't exist")
691
+ found = [x for x in found if x is not None]
563
692
 
564
- if view_result and document_result:
693
+ if len(found) > 1:
565
694
  raise KeyError("Ambiguous ID search result")
566
- if not (view_result or document_result):
695
+ elif len(found) == 0:
567
696
  raise KeyError("ID not found: %s" % item)
568
- return anno_result or view_result or document_result
569
- # pytype: enable=bad-return-type
570
-
571
-
572
- class MmifMetadata(MmifObject):
573
- """
574
- Basic MmifObject class to contain the top-level metadata of a MMIF file.
575
-
576
- :param metadata_obj: the JSON data
577
- """
578
-
579
- def __init__(self, metadata_obj: Optional[Union[bytes, str, dict]] = None) -> None:
580
- # TODO (krim @ 10/7/20): there could be a better name and a better way to give a value to this
581
- self.mmif: str = f"http://mmif.clams.ai/{mmif.__specver__}"
582
- self._required_attributes = ["mmif"]
583
- super().__init__(metadata_obj)
584
-
585
-
586
- class DocumentsList(DataList[Document]):
587
- """
588
- DocumentsList object that implements :class:`mmif.serialize.model.DataList`
589
- for :class:`mmif.serialize.document.Document`.
590
- """
591
- _items: Dict[str, Document]
592
-
593
- def _deserialize(self, input_list: list) -> None: # pytype: disable=signature-mismatch
594
- """
595
- Extends base ``_deserialize`` method to initialize ``items`` as a dict from
596
- document IDs to :class:`mmif.serialize.document.Document` objects.
597
-
598
- :param input_list: the JSON data that defines the list of documents
599
- :return: None
600
- """
601
- self._items = {item['properties']['id']: Document(item) for item in input_list}
602
-
603
- def append(self, value: Document, overwrite=False) -> None:
604
- """
605
- Appends a document to the list.
606
-
607
- Fails if there is already a document with the same ID
608
- in the list, unless ``overwrite`` is set to True.
609
-
610
- :param value: the :class:`mmif.serialize.document.Document`
611
- object to add
612
- :param overwrite: if set to True, will overwrite an
613
- existing document with the same ID
614
- :raises KeyError: if ``overwrite`` is set to False and
615
- a document with the same ID exists
616
- in the list
617
- :return: None
618
- """
619
- super()._append_with_key(value.id, value, overwrite)
620
-
621
-
622
- class ViewsList(DataList[View]):
623
- """
624
- ViewsList object that implements :class:`mmif.serialize.model.DataList`
625
- for :class:`mmif.serialize.view.View`.
626
- """
627
- _items: Dict[str, View]
628
-
629
- def __init__(self, mmif_obj: Optional[Union[bytes, str, list]] = None):
630
- super().__init__(mmif_obj)
631
-
632
- def _deserialize(self, input_list: list) -> None: # pytype: disable=signature-mismatch
633
- """
634
- Extends base ``_deserialize`` method to initialize ``items`` as a dict from
635
- view IDs to :class:`mmif.serialize.view.View` objects.
636
-
637
- :param input_list: the JSON data that defines the list of views
638
- :return: None
639
- """
640
- if input_list:
641
- self._items = {item['id']: View(item) for item in input_list}
642
-
643
- def append(self, value: View, overwrite=False) -> None:
644
- """
645
- Appends a view to the list.
646
-
647
- Fails if there is already a view with the same ID
648
- in the list, unless ``overwrite`` is set to True.
649
-
650
- :param value: the :class:`mmif.serialize.view.View`
651
- object to add
652
- :param overwrite: if set to True, will overwrite an
653
- existing view with the same ID
654
- :raises KeyError: if ``overwrite`` is set to False and
655
- a view with the same ID exists
656
- in the list
657
- :return: None
658
- """
659
- super()._append_with_key(value.id, value, overwrite)
660
-
661
- def get_last(self) -> Optional[View]:
662
- """
663
- Returns the last view appended to the list.
664
- """
665
- for view in reversed(self._items.values()):
666
- if 'error' not in view.metadata and 'warning' not in view.metadata:
667
- return view
697
+ else:
698
+ return found[-1]
@@ -14,7 +14,7 @@ for the different components of MMIF is added in the subclasses.
14
14
 
15
15
  import json
16
16
  from datetime import datetime
17
- from typing import Union, Any, Dict, Optional, TypeVar, Generic, Generator, Iterator, Type, Set
17
+ from typing import Union, Any, Dict, Optional, TypeVar, Generic, Generator, Iterator, Type, Set, ClassVar
18
18
 
19
19
  from deepdiff import DeepDiff
20
20
 
@@ -44,41 +44,48 @@ class MmifObject(object):
44
44
 
45
45
  1. _unnamed_attributes:
46
46
  Only can be either None or an empty dictionary. If it's set to None,
47
- it means the class won't take any ``Additional Attributes`` in the JSON
48
- schema sense. If it's a dict, users can throw any k-v pairs to the
49
- class, EXCEPT for the reserved two key names.
47
+ it means the class won't take any ``Additional Attributes`` in the
48
+ JSON schema sense. If it's an empty dict, users can throw any k-v
49
+ pairs to the class, as long as the key is not a "reserved" name,
50
+ and those additional attributes will be stored in this dict while
51
+ in memory.
50
52
  2. _attribute_classes:
51
53
  This is a dict from a key name to a specific python class to use for
52
54
  deserialize the value. Note that a key name in this dict does NOT
53
55
  have to be a *named* attribute, but is recommended to be one.
54
56
  3. _required_attributes:
55
- This is a simple list of names of attributes that are required in the object.
56
- When serialize, an object will skip its *empty* (e.g. zero-length, or None)
57
- attributes unless they are in this list. Otherwise, the serialized JSON
58
- string would have empty representations (e.g. ``""``, ``[]``).
57
+ This is a simple list of names of attributes that are required in
58
+ the object. When serialize, an object will skip its *empty* (e.g.
59
+ zero-length, or None) attributes unless they are in this list.
60
+ Otherwise, the serialized JSON string would have empty
61
+ representations (e.g. ``""``, ``[]``).
59
62
  4. _exclude_from_diff:
60
- This is a simple list of names of attributes that should be excluded from
61
- the diff calculation in ``__eq__``.
63
+ This is a simple list of names of attributes that should be excluded
64
+ from the diff calculation in ``__eq__``.
62
65
 
63
66
  # TODO (krim @ 8/17/20): this dict is however, a duplicate with the type hints in the class definition.
64
- Maybe there is a better way to utilize type hints (e.g. getting them as a programmatically), but for now
65
- developers should be careful to add types to hints as well as to this dict.
67
+ Maybe there is a better way to utilize type hints (e.g. getting them
68
+ as a programmatically), but for now developers should be careful to
69
+ add types to hints as well as to this dict.
66
70
 
67
71
  Also note that those special attributes MUST be set in the __init__()
68
72
  before calling super method, otherwise deserialization will not work.
69
73
 
70
74
  And also, a subclass that has one or more *named* attributes, it must
71
- set those attributes in the __init__() before calling super method. When
72
- serializing a MmifObject, all *empty* attributes will be ignored, so for
73
- optional named attributes, you must leave the values empty (len == 0), but
74
- NOT None. Any None-valued named attributes will cause issues with current
75
- implementation.
75
+ set those attributes in the __init__() before calling super method.
76
+ When serializing a MmifObject, all *empty* attributes will be ignored,
77
+ so for optional named attributes, you must leave the values empty
78
+ (len == 0), but NOT None. Any None-valued named attributes will cause
79
+ issues with current implementation.
76
80
 
77
81
  :param mmif_obj: JSON string or `dict` to initialize an object.
78
82
  If not given, an empty object will be initialized, sometimes with
79
83
  an ID value automatically generated, based on its parent object.
80
84
  """
81
85
 
86
+ view_prefix: ClassVar[str] = 'v_'
87
+ id_delimiter: ClassVar[str] = ':'
88
+
82
89
  # these are the reserved names that cannot be used as attribute names, and
83
90
  # they won't be serialized
84
91
  reserved_names: Set[str] = {
@@ -269,8 +276,8 @@ class MmifObject(object):
269
276
  """
270
277
  Returns number of attributes that are not *empty*.
271
278
  """
272
- return sum([named in self and not self.is_empty(self[named]) for named in self._named_attributes()]) \
273
- + (len(self._unnamed_attributes) if self._unnamed_attributes else 0)
279
+ return (sum([named in self and not self.is_empty(self[named]) for named in self._named_attributes()])
280
+ + (len(self._unnamed_attributes) if self._unnamed_attributes else 0))
274
281
 
275
282
  def __setitem__(self, key, value) -> None:
276
283
  if key in self.reserved_names: