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.
- mmif_python-1.0.15/PKG-INFO +46 -0
- mmif_python-1.0.15/VERSION +1 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/serialize/annotation.py +34 -15
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/serialize/mmif.py +160 -129
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/serialize/model.py +26 -19
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/serialize/view.py +42 -4
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/utils/timeunit_helper.py +4 -4
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/utils/video_document_helper.py +50 -10
- mmif_python-1.0.15/mmif/ver/__init__.py +2 -0
- mmif_python-1.0.15/mmif_python.egg-info/PKG-INFO +46 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif_python.egg-info/SOURCES.txt +5 -1
- mmif_python-1.0.15/tests/test_serialize.py +1354 -0
- mmif_python-1.0.15/tests/test_utils.py +176 -0
- mmif_python-1.0.15/tests/test_versioncompat.py +56 -0
- mmif_python-1.0.15/tests/test_vocab.py +56 -0
- mmif-python-1.0.11/PKG-INFO +0 -37
- mmif-python-1.0.11/VERSION +0 -1
- mmif-python-1.0.11/mmif/ver/__init__.py +0 -2
- mmif-python-1.0.11/mmif_python.egg-info/PKG-INFO +0 -37
- {mmif-python-1.0.11 → mmif_python-1.0.15}/LICENSE +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/MANIFEST.in +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/README.md +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/__init__.py +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/res/__init__.py +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/res/clams.vocabulary.yaml +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/res/do-not-edit.txt +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/res/mmif.json +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/serialize/__init__.py +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/utils/__init__.py +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/utils/sequence_helper.py +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/ver/do-not-edit.txt +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/vocabulary/__init__.py +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/vocabulary/annotation_types.py +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/vocabulary/base_types.py +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/vocabulary/do-not-edit.txt +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif/vocabulary/document_types.py +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif_docloc_http/__init__.py +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif_python.egg-info/dependency_links.txt +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif_python.egg-info/requires.txt +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/mmif_python.egg-info/top_level.txt +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/requirements.cv +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/requirements.seq +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/requirements.txt +0 -0
- {mmif-python-1.0.11 → mmif_python-1.0.15}/setup.cfg +0 -0
- {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
|
|
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
|
|
405
|
-
|
|
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
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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,
|
|
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
|
|
366
|
-
vid, did = doc_id.split(
|
|
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
|
|
414
|
-
view_id, ann_id = ann_id.split(
|
|
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
|
|
442
|
-
vid, did = doc_id.split(
|
|
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
|
-
|
|
572
|
+
get_views_contain = get_all_views_contain
|
|
573
|
+
|
|
574
|
+
def get_view_with_error(self) -> Optional[View]:
|
|
468
575
|
"""
|
|
469
|
-
|
|
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
|
-
|
|
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
|
|
510
|
-
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
|
-
|
|
541
|
-
|
|
661
|
+
def __getitem__(self, item: str) \
|
|
662
|
+
-> Union[Document, View, Annotation, MmifMetadata, DocumentsList, ViewsList]:
|
|
542
663
|
"""
|
|
543
|
-
getitem implementation for Mmif.
|
|
544
|
-
rather than returning a None
|
|
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:
|
|
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(
|
|
676
|
+
split_attempt = item.split(self.id_delimiter)
|
|
553
677
|
|
|
554
|
-
|
|
555
|
-
view_result = self.views.get(split_attempt[0])
|
|
678
|
+
found = []
|
|
556
679
|
|
|
557
680
|
if len(split_attempt) == 1:
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
|
693
|
+
if len(found) > 1:
|
|
565
694
|
raise KeyError("Ambiguous ID search result")
|
|
566
|
-
|
|
695
|
+
elif len(found) == 0:
|
|
567
696
|
raise KeyError("ID not found: %s" % item)
|
|
568
|
-
|
|
569
|
-
|
|
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
|
|
48
|
-
schema sense. If it's
|
|
49
|
-
class,
|
|
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
|
|
56
|
-
When serialize, an object will skip its *empty* (e.g.
|
|
57
|
-
attributes unless they are in this list.
|
|
58
|
-
string would have empty
|
|
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
|
|
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
|
|
65
|
-
|
|
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.
|
|
72
|
-
serializing a MmifObject, all *empty* attributes will be ignored,
|
|
73
|
-
optional named attributes, you must leave the values empty
|
|
74
|
-
NOT None. Any None-valued named attributes will cause
|
|
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
|
-
|
|
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:
|