mmif-python 1.5.2__tar.gz → 1.5.4__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.5.2 → mmif_python-1.5.4}/CHANGELOG.md +18 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/PKG-INFO +2 -2
- {mmif_python-1.5.2 → mmif_python-1.5.4}/documentation/target-versions.csv +2 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/serialize/annotation.py +125 -65
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/serialize/mmif.py +75 -22
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/serialize/view.py +17 -8
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/summarizer/nodes.py +64 -25
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/summarizer/summary.py +3 -3
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/video_document_helper.py +217 -37
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif_python.egg-info/PKG-INFO +2 -2
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif_python.egg-info/requires.txt +1 -1
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif_python.egg-info/scm_file_list.json +64 -64
- mmif_python-1.5.4/mmif_python.egg-info/scm_version.json +8 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/pyproject.toml +1 -1
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/test_serialize.py +227 -35
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/test_utils.py +425 -57
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/test_utils_cli.py +23 -0
- mmif_python-1.5.2/mmif_python.egg-info/scm_version.json +0 -8
- {mmif_python-1.5.2 → mmif_python-1.5.4}/.github/workflows/codecov.yml +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/.github/workflows/issue-assign.yml +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/.github/workflows/issue-close.yml +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/.github/workflows/issue-infra-project.yml +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/.github/workflows/publish.yml +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/.github/workflows/release-check.yml +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/.gitignore +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/CONTRIBUTING.md +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/LICENSE +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/README.md +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/build-tools/build.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/build-tools/clean.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/build-tools/docs.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/build-tools/prep_release.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/build-tools/publish.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/build-tools/test.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/documentation/_mmif_example_builder/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/documentation/_templates/versions.html +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/documentation/cli.rst +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/documentation/conf.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/documentation/consumer-tutorial.rst +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/documentation/index.rst +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/documentation/introduction.rst +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/documentation/plugins.rst +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/documentation/summarizer.rst +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/documentation/target-versions.rst +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/res/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/res/mmif.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/serialize/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/serialize/model.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/cli/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/cli/describe.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/cli/rewind.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/cli/source.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/cli/summarize.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/sequence_helper.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/summarizer/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/summarizer/config.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/summarizer/graph.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/summarizer/utils.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/text_document_helper.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/timeunit_helper.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/utils/workflow_helper.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/ver/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/vocabulary/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/vocabulary/annotation_types.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/vocabulary/base_types.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif/vocabulary/document_types.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif_docloc_http/__init__.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif_python.egg-info/SOURCES.txt +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif_python.egg-info/dependency_links.txt +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif_python.egg-info/entry_points.txt +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/mmif_python.egg-info/top_level.txt +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/setup.cfg +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/black-2997fps.mp4 +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/black-2997fps.webm +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/1.0.5-old-shortid.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/README.md +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/bars-tones-slates/raw.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/east-tesseract-typing/raw.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/everything/raw.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/examples.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/others/README.md +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/others/image-box-ocr-timex.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/others/image-box-ocr.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/others/image-box.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/others/image.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/others/slate-detect.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/others/slate-reco.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/others/video-demux.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/others/video-transcript-demux-fa.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/others/video-transcript-demux-fa.short.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/others/video-transcript-demux.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/others/video-transcript-sentencesplit.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/others/video-transcript.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/others/video.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/segmenter-kaldi-ner/raw.json +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif-examples/swt-1.0.mmif +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/mmif_examples.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/test_specver.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/test_vocab.py +0 -0
- {mmif_python-1.5.2 → mmif_python-1.5.4}/tests/testsrc-2997fps-ptsoffset.mp4 +0 -0
|
@@ -1,4 +1,22 @@
|
|
|
1
1
|
|
|
2
|
+
## releasing 1.5.3 (2026-07-08)
|
|
3
|
+
### Overview
|
|
4
|
+
This patch includes improvements in automatic conversion between contains-metadata properties and annotation properties (#396)
|
|
5
|
+
|
|
6
|
+
### Additions
|
|
7
|
+
* automatic "factoring" of value-shared properties from annotation-level to contains-level at serialization time works much more generally now (formerly only worked for `Annotation` type and `document` prop)
|
|
8
|
+
|
|
9
|
+
### Changes
|
|
10
|
+
* fixed a bug in reading old MMIF files with non-canonical prop names (contains-level prop value was not properly propagated at deserialization for those non-canonical named props)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
## releasing 1.5.2 (2026-07-07)
|
|
15
|
+
### Overview
|
|
16
|
+
This is a quick patch on the bug that when MMIF is deserialized, the annotation ID counts in the views weren't properly initialized. This should not be a real bug since new annotations should always written to new views (of current processing app) but not to existing views (where annotation count > 0), however, bugs in app code can trigger this condition, and this patch preventively fixes such cases.
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
|
2
20
|
## releasing 1.5.1 (2026-06-19)
|
|
3
21
|
### Overview
|
|
4
22
|
This patch contains a bug fix in VDH that couldn't handle some mkv-contained video files (https://github.com/clamsproject/mmif-python/issues/390) , and some other minor documentation generation bugs.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mmif-python
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.4
|
|
4
4
|
Summary: Python implementation of MultiMedia Interchange Format specification. (https://mmif.clams.ai)
|
|
5
5
|
License-Expression: Apache-2.0
|
|
6
6
|
Project-URL: homepage, https://mmif.clams.ai
|
|
@@ -21,7 +21,7 @@ Requires-Dist: pillow; extra == "cv"
|
|
|
21
21
|
Requires-Dist: opencv-python; extra == "cv"
|
|
22
22
|
Requires-Dist: ffmpeg-python; extra == "cv"
|
|
23
23
|
Requires-Dist: wurlitzer; extra == "cv"
|
|
24
|
-
Requires-Dist: av; extra == "cv"
|
|
24
|
+
Requires-Dist: av>=15; extra == "cv"
|
|
25
25
|
Provides-Extra: seq
|
|
26
26
|
Requires-Dist: numpy; extra == "seq"
|
|
27
27
|
Provides-Extra: docs
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
"``mmif-python`` version","Target MMIF Specification"
|
|
2
|
+
`1.5.4 <https://pypi.org/project/mmif-python/1.5.4/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
|
|
3
|
+
`1.5.3 <https://pypi.org/project/mmif-python/1.5.3/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
|
|
2
4
|
`1.5.2 <https://pypi.org/project/mmif-python/1.5.2/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
|
|
3
5
|
`1.5.1 <https://pypi.org/project/mmif-python/1.5.1/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
|
|
4
6
|
`1.5.0 <https://pypi.org/project/mmif-python/1.5.0/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
|
|
@@ -6,6 +6,7 @@ In MMIF, annotations are created by apps in a workflow as a part
|
|
|
6
6
|
of a view. For documentation on how views are represented, see
|
|
7
7
|
:mod:`mmif.serialize.view`.
|
|
8
8
|
"""
|
|
9
|
+
import functools
|
|
9
10
|
import importlib
|
|
10
11
|
import itertools
|
|
11
12
|
import os
|
|
@@ -22,7 +23,7 @@ from .model import MmifObject, PRMTV_TYPES
|
|
|
22
23
|
from .. import DocumentTypes, AnnotationTypes
|
|
23
24
|
import mmif_docloc_http
|
|
24
25
|
|
|
25
|
-
__all__ = ['Annotation', 'AnnotationProperties', 'Document', 'DocumentProperties', 'Text']
|
|
26
|
+
__all__ = ['Annotation', 'AnnotationProperties', 'Document', 'DocumentProperties', 'Text', 'canonicalize_prop_name']
|
|
26
27
|
|
|
27
28
|
T = TypeVar('T')
|
|
28
29
|
LIST_PRMTV = typing.List[PRMTV_TYPES] # list of values (most cases for annotation props)
|
|
@@ -41,6 +42,40 @@ discovered_docloc_plugins.update({
|
|
|
41
42
|
})
|
|
42
43
|
|
|
43
44
|
|
|
45
|
+
@functools.lru_cache(maxsize=None)
|
|
46
|
+
def _prop_alias_map(shortname: str) -> Dict[str, str]:
|
|
47
|
+
"""
|
|
48
|
+
Maps every spelling of an aliased property of a type to the canonical
|
|
49
|
+
name. The table is read from the latest version of the type in the
|
|
50
|
+
vocabulary (``AnnotationTypes.<shortname>``), which is the single source
|
|
51
|
+
of truth for property aliases.
|
|
52
|
+
|
|
53
|
+
:param shortname: shortname of the annotation type (e.g. ``TimeFrame``)
|
|
54
|
+
:return: ``{spelling: canonical_name}``, empty for types without aliases
|
|
55
|
+
"""
|
|
56
|
+
latest = getattr(AnnotationTypes, shortname, None)
|
|
57
|
+
groups = getattr(latest, '_property_aliases', None) or {}
|
|
58
|
+
return {alias: canonical for canonical, group in groups.items() for alias in group}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def canonicalize_prop_name(at_type: ThingTypesBase, prop_name: str) -> str:
|
|
62
|
+
"""
|
|
63
|
+
Resolves a property name to its canonical spelling for the given type.
|
|
64
|
+
Property aliases were introduced in MMIF 1.0.2, when the general
|
|
65
|
+
``label`` property replaced ``frameType`` (``TimeFrame``) and ``boxType``
|
|
66
|
+
(``BoundingBox``). Names that are not aliases, and types without an alias
|
|
67
|
+
table, are returned unchanged.
|
|
68
|
+
|
|
69
|
+
:param at_type: the ``@type`` of the annotation
|
|
70
|
+
:param prop_name: a property name, canonical or deprecated
|
|
71
|
+
:return: the canonical property name
|
|
72
|
+
"""
|
|
73
|
+
shortname = getattr(at_type, 'shortname', None)
|
|
74
|
+
if not shortname:
|
|
75
|
+
return prop_name
|
|
76
|
+
return _prop_alias_map(shortname).get(prop_name, prop_name)
|
|
77
|
+
|
|
78
|
+
|
|
44
79
|
class Annotation(MmifObject):
|
|
45
80
|
"""
|
|
46
81
|
MmifObject that represents an annotation in a MMIF view.
|
|
@@ -49,11 +84,11 @@ class Annotation(MmifObject):
|
|
|
49
84
|
def __init__(self, anno_obj: Optional[Union[bytes, str, dict]] = None, *_) -> None:
|
|
50
85
|
self._type: ThingTypesBase = ThingTypesBase('')
|
|
51
86
|
# to store the parent view ID
|
|
52
|
-
self._props_ephemeral: AnnotationProperties = AnnotationProperties()
|
|
87
|
+
self._props_ephemeral: AnnotationProperties = AnnotationProperties(None, self)
|
|
53
88
|
self._alignments = {} # to hold alignment information (Alignment anno long_id -> aligned anno long_id)
|
|
54
89
|
self.reserved_names.update(('_props_ephemeral', '_alignments'))
|
|
55
90
|
if not hasattr(self, 'properties'): # don't overwrite DocumentProperties on super() call
|
|
56
|
-
self.properties: AnnotationProperties = AnnotationProperties()
|
|
91
|
+
self.properties: AnnotationProperties = AnnotationProperties(None, self)
|
|
57
92
|
self._attribute_classes = {'properties': AnnotationProperties}
|
|
58
93
|
self.disallow_additional_properties()
|
|
59
94
|
self._required_attributes = ["_type", "properties"]
|
|
@@ -69,9 +104,7 @@ class Annotation(MmifObject):
|
|
|
69
104
|
# here is the place to parse formatted IDs and store prefixes in the parent mmif object.
|
|
70
105
|
# (see https://github.com/clamsproject/mmif/issues/64#issuecomment-849241309 for discussion)
|
|
71
106
|
super()._deserialize(input_dict)
|
|
72
|
-
|
|
73
|
-
self._add_prop_aliases(k, v)
|
|
74
|
-
|
|
107
|
+
|
|
75
108
|
def _cache_alignment(self, alignment_ann: 'Annotation', alignedto_ann: 'Annotation') -> None:
|
|
76
109
|
"""
|
|
77
110
|
Cache alignment information. This cache will not be serialized.
|
|
@@ -107,29 +140,20 @@ class Annotation(MmifObject):
|
|
|
107
140
|
yield aligned
|
|
108
141
|
|
|
109
142
|
|
|
110
|
-
def
|
|
111
|
-
"""
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if alias in self.properties.keys():
|
|
125
|
-
warning_msg = (f'Both "{key_to_add}" and "{alias}" are in the properties of "{self.id}", '
|
|
126
|
-
f'however ')
|
|
127
|
-
if alias == alias_rep:
|
|
128
|
-
warning_msg += f'"{key_to_add}" is an alias of "{alias_rep}".'
|
|
129
|
-
else:
|
|
130
|
-
warning_msg += f'"{key_to_add}" and "{alias}" are both aliases of "{alias_rep}".'
|
|
131
|
-
warning_msg += f'Having two synonyms in the same annotation can cause unexpected behavior. '
|
|
132
|
-
warnings.warn(warning_msg, UserWarning)
|
|
143
|
+
def _add_ephemeral_defaults(self, defaults: typing.Mapping[str, typing.Any]) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Distributes view-level defaults (from the ``contains`` metadata of the
|
|
146
|
+
parent view) to this annotation as ephemeral properties, under their
|
|
147
|
+
canonical names. An annotation-level value takes precedence over a
|
|
148
|
+
view-level default, so a default is skipped when the annotation
|
|
149
|
+
carries the property itself.
|
|
150
|
+
|
|
151
|
+
:param defaults: property defaults for this annotation's ``@type``
|
|
152
|
+
"""
|
|
153
|
+
for name, value in defaults.items():
|
|
154
|
+
name = canonicalize_prop_name(self._type, name)
|
|
155
|
+
if name not in self.properties:
|
|
156
|
+
self._props_ephemeral[name] = value
|
|
133
157
|
|
|
134
158
|
def is_type(self, at_type: Union[str, ThingTypesBase]) -> bool:
|
|
135
159
|
"""
|
|
@@ -239,7 +263,9 @@ class Annotation(MmifObject):
|
|
|
239
263
|
def add_property(self, name: str,
|
|
240
264
|
value: Union[PRMTV_TYPES, LIST_PRMTV, LIST_LIST_PRMTV, DICT_PRMTV, DICT_LIST_PRMTV]) -> None:
|
|
241
265
|
"""
|
|
242
|
-
Adds a property to the annotation's properties.
|
|
266
|
+
Adds a property to the annotation's properties. A deprecated spelling
|
|
267
|
+
of an aliased property (e.g. ``frameType``) is stored under its
|
|
268
|
+
canonical name (``label``), with a :class:`DeprecationWarning`.
|
|
243
269
|
|
|
244
270
|
:param name: the name of the property
|
|
245
271
|
:param value: the property's desired value
|
|
@@ -252,14 +278,14 @@ class Annotation(MmifObject):
|
|
|
252
278
|
# "either string, number, boolean, None, a JSON array of them, "
|
|
253
279
|
# "or a JSON object of them keyed by strings."
|
|
254
280
|
# f"(\"{name}\": \"{str(value)}\"")
|
|
255
|
-
self._add_prop_aliases(name, value)
|
|
256
281
|
|
|
257
282
|
def __getitem__(self, prop_name: str):
|
|
258
283
|
if prop_name in {'at_type', '@type'}:
|
|
259
284
|
return str(self._type)
|
|
260
285
|
elif prop_name == 'properties':
|
|
261
286
|
return self.properties
|
|
262
|
-
|
|
287
|
+
prop_name = canonicalize_prop_name(self._type, prop_name)
|
|
288
|
+
if prop_name in self.properties:
|
|
263
289
|
return self.properties[prop_name]
|
|
264
290
|
elif prop_name in self._props_ephemeral:
|
|
265
291
|
return self._props_ephemeral[prop_name]
|
|
@@ -279,7 +305,9 @@ class Annotation(MmifObject):
|
|
|
279
305
|
3. Special fields (``@type``, ``properties``)
|
|
280
306
|
|
|
281
307
|
This allows convenient access to properties without explicitly
|
|
282
|
-
checking the ``properties`` object or view-level metadata.
|
|
308
|
+
checking the ``properties`` object or view-level metadata. A
|
|
309
|
+
deprecated spelling of an aliased property (e.g. ``frameType``)
|
|
310
|
+
resolves to the canonical property (``label``).
|
|
283
311
|
|
|
284
312
|
:param prop_name: The name of the property to retrieve
|
|
285
313
|
:param default: The value to return if the property is not found (default: None)
|
|
@@ -318,23 +346,6 @@ class Annotation(MmifObject):
|
|
|
318
346
|
except KeyError:
|
|
319
347
|
return False
|
|
320
348
|
|
|
321
|
-
def _get_label(self) -> str:
|
|
322
|
-
"""
|
|
323
|
-
Another prototypical method to handle property aliases.
|
|
324
|
-
See :meth:`.Annotation._add_prop_aliases` for more details on
|
|
325
|
-
what property aliases are.
|
|
326
|
-
Not recommended to use this method as `_add_prop_aliases` method
|
|
327
|
-
is preferred.
|
|
328
|
-
"""
|
|
329
|
-
if 'label' in self:
|
|
330
|
-
return str(self.get('label'))
|
|
331
|
-
elif self._type.shortname == 'TimeFrame' and 'frameType' in self:
|
|
332
|
-
return str(self.get('frameType'))
|
|
333
|
-
elif self._type.shortname == 'BoundingBox' and 'boxType' in self:
|
|
334
|
-
return str(self.get('boxType'))
|
|
335
|
-
else:
|
|
336
|
-
raise KeyError("No label found in this annotation.")
|
|
337
|
-
|
|
338
349
|
def is_document(self):
|
|
339
350
|
return isinstance(self._type, DocumentTypesBase)
|
|
340
351
|
|
|
@@ -356,8 +367,8 @@ class Document(Annotation):
|
|
|
356
367
|
# see https://github.com/clamsproject/mmif-python/issues/226 for discussion
|
|
357
368
|
# around the use of these three dictionaries
|
|
358
369
|
# (names changed since, `existing` >> `ephemeral` and `temporary` >> `pending`)
|
|
359
|
-
self._props_original: DocumentProperties = DocumentProperties()
|
|
360
|
-
self._props_pending: AnnotationProperties = AnnotationProperties()
|
|
370
|
+
self._props_original: DocumentProperties = DocumentProperties(None, self)
|
|
371
|
+
self._props_pending: AnnotationProperties = AnnotationProperties(None, self)
|
|
361
372
|
self.reserved_names.update(('_props_original', '_props_pending'))
|
|
362
373
|
|
|
363
374
|
self._type: Union[ThingTypesBase, DocumentTypesBase] = ThingTypesBase('')
|
|
@@ -578,6 +589,7 @@ class AnnotationProperties(MmifObject, MutableMapping[str, T]):
|
|
|
578
589
|
"""
|
|
579
590
|
|
|
580
591
|
def __delitem__(self, key: str) -> None:
|
|
592
|
+
key = self._canonical(key)
|
|
581
593
|
frm = None
|
|
582
594
|
for k in self.__iter__():
|
|
583
595
|
if k == key:
|
|
@@ -593,36 +605,83 @@ class AnnotationProperties(MmifObject, MutableMapping[str, T]):
|
|
|
593
605
|
del frm[key]
|
|
594
606
|
else:
|
|
595
607
|
raise KeyError(f'Key "{key}" not found.')
|
|
596
|
-
|
|
608
|
+
|
|
597
609
|
def __iter__(self) -> Iterator[str]:
|
|
598
610
|
"""
|
|
599
|
-
``__iter__`` on Mapping should basically work as ``keys()`` method
|
|
611
|
+
``__iter__`` on Mapping should basically work as ``keys()`` method
|
|
600
612
|
of vanilla dict.
|
|
601
613
|
"""
|
|
602
614
|
for key in itertools.chain(self._named_attributes(), self._unnamed_attributes):
|
|
603
615
|
yield key
|
|
604
|
-
|
|
616
|
+
|
|
605
617
|
def __getitem__(self, key):
|
|
606
618
|
"""
|
|
607
|
-
Parent MmifObject class has a __getitem__ method that checks if
|
|
608
|
-
the value is empty when asked for an unnamed attribute. But for
|
|
609
|
-
AnnotationProperties, any arbitrary property that's added
|
|
610
|
-
explicitly by the user (developer) should not be ignored and
|
|
611
|
-
returned even the value is empty.
|
|
619
|
+
Parent MmifObject class has a __getitem__ method that checks if
|
|
620
|
+
the value is empty when asked for an unnamed attribute. But for
|
|
621
|
+
AnnotationProperties, any arbitrary property that's added
|
|
622
|
+
explicitly by the user (developer) should not be ignored and
|
|
623
|
+
returned even the value is empty.
|
|
612
624
|
"""
|
|
625
|
+
key = self._canonical(key)
|
|
613
626
|
if key in self._named_attributes():
|
|
614
627
|
return self.__dict__[key]
|
|
615
628
|
else:
|
|
616
629
|
return self._unnamed_attributes[key]
|
|
617
630
|
|
|
618
|
-
def
|
|
631
|
+
def __setitem__(self, key: str, value) -> None:
|
|
632
|
+
"""
|
|
633
|
+
Sets a property. A deprecated spelling of an aliased property (e.g.
|
|
634
|
+
``frameType``) is stored under its canonical name (``label``), with
|
|
635
|
+
a :class:`DeprecationWarning`.
|
|
636
|
+
"""
|
|
637
|
+
canonical = self._canonical(key)
|
|
638
|
+
if canonical != key:
|
|
639
|
+
warnings.warn(f'"{key}" is a deprecated alias of "{canonical}", storing the value as "{canonical}".',
|
|
640
|
+
DeprecationWarning)
|
|
641
|
+
super().__setitem__(canonical, value)
|
|
642
|
+
|
|
643
|
+
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None,
|
|
644
|
+
parent: Optional['Annotation'] = None, *_) -> None:
|
|
619
645
|
self.id: str = ''
|
|
620
646
|
# any individual at_type (subclassing this class) can have its own set of required attributes
|
|
621
647
|
self._required_attributes = ["id"]
|
|
622
648
|
# allowing additional attributes for arbitrary annotation properties
|
|
623
649
|
self._unnamed_attributes = {}
|
|
650
|
+
# the Annotation these properties belong to; its `@type` decides which
|
|
651
|
+
# property names are aliases of each other (see `canonicalize_prop_name`)
|
|
652
|
+
self._parent_annotation = parent
|
|
653
|
+
self.reserved_names.add('_parent_annotation')
|
|
624
654
|
super().__init__(mmif_obj)
|
|
625
655
|
|
|
656
|
+
def _canonical(self, key: str) -> str:
|
|
657
|
+
if self._parent_annotation is None:
|
|
658
|
+
return key
|
|
659
|
+
return canonicalize_prop_name(self._parent_annotation._type, key)
|
|
660
|
+
|
|
661
|
+
def _deserialize(self, input_dict: dict) -> None:
|
|
662
|
+
"""
|
|
663
|
+
Deserializes the properties, rewriting deprecated spellings to their
|
|
664
|
+
canonical names so that there is one key per property. When two
|
|
665
|
+
spellings of the same property are present, the canonical one wins
|
|
666
|
+
(or the first one seen, when neither is canonical), with a warning.
|
|
667
|
+
"""
|
|
668
|
+
canonical_dict = {}
|
|
669
|
+
# canonical name -> the deprecated spelling it was read from
|
|
670
|
+
from_alias = {}
|
|
671
|
+
for key, value in input_dict.items():
|
|
672
|
+
canonical = self._canonical(key)
|
|
673
|
+
if canonical in canonical_dict and (canonical != key or canonical in from_alias):
|
|
674
|
+
alias = key if canonical != key else from_alias[canonical]
|
|
675
|
+
warnings.warn(f'Both "{alias}" and "{canonical}" are in the properties of '
|
|
676
|
+
f'"{input_dict.get("id", "")}", but "{alias}" is an alias of "{canonical}". '
|
|
677
|
+
f'Keeping the value of "{canonical}".', UserWarning)
|
|
678
|
+
if canonical != key:
|
|
679
|
+
continue
|
|
680
|
+
canonical_dict[canonical] = value
|
|
681
|
+
if canonical != key:
|
|
682
|
+
from_alias[canonical] = key
|
|
683
|
+
super()._deserialize(canonical_dict)
|
|
684
|
+
|
|
626
685
|
|
|
627
686
|
class DocumentProperties(AnnotationProperties):
|
|
628
687
|
"""
|
|
@@ -632,7 +691,8 @@ class DocumentProperties(AnnotationProperties):
|
|
|
632
691
|
:param mmif_obj: the JSON data that defines the properties
|
|
633
692
|
"""
|
|
634
693
|
|
|
635
|
-
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None,
|
|
694
|
+
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None,
|
|
695
|
+
parent: Optional['Annotation'] = None, *_) -> None:
|
|
636
696
|
self.mime: str = ''
|
|
637
697
|
# note the trailing underscore here. I wanted to use the name `location`
|
|
638
698
|
# for @property in this class and `Document` class, so had to use a diff
|
|
@@ -643,9 +703,9 @@ class DocumentProperties(AnnotationProperties):
|
|
|
643
703
|
self._attribute_classes = {'text': Text}
|
|
644
704
|
# in theory, either `location` or `text` should appear in a `document`
|
|
645
705
|
# but with current implementation, there's no easy way to set a condition
|
|
646
|
-
# for `oneOf` requirement
|
|
647
|
-
# see MmifObject::_required_attributes in model.py
|
|
648
|
-
super().__init__(mmif_obj)
|
|
706
|
+
# for `oneOf` requirement
|
|
707
|
+
# see MmifObject::_required_attributes in model.py
|
|
708
|
+
super().__init__(mmif_obj, parent)
|
|
649
709
|
|
|
650
710
|
def _deserialize(self, input_dict: dict) -> None:
|
|
651
711
|
if "location" in input_dict:
|
|
@@ -175,6 +175,14 @@ class Mmif(MmifObject):
|
|
|
175
175
|
all_views = mmif.views[1:4] # Slice of views
|
|
176
176
|
"""
|
|
177
177
|
|
|
178
|
+
#: Hardcoded list of property names that must stay at the individual
|
|
179
|
+
#: annotation level, and never "factored" into the view's ``contains``
|
|
180
|
+
#: metadata, even when redundant across annotations.
|
|
181
|
+
_NON_FACTORABLE_PROPS = frozenset({
|
|
182
|
+
'start', 'end', 'targets', 'source', 'target', 'representatives',
|
|
183
|
+
'mime', 'location', 'location_', 'text',
|
|
184
|
+
})
|
|
185
|
+
|
|
178
186
|
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None, *, validate: bool = True) -> None:
|
|
179
187
|
self.metadata: MmifMetadata = MmifMetadata()
|
|
180
188
|
self.documents: DocumentsList = DocumentsList()
|
|
@@ -211,26 +219,42 @@ class Mmif(MmifObject):
|
|
|
211
219
|
json_str = json.loads(json_str)
|
|
212
220
|
jsonschema.validators.validate(json_str, schema)
|
|
213
221
|
|
|
214
|
-
def serialize(self,
|
|
222
|
+
def serialize(self,
|
|
223
|
+
pretty: bool = False,
|
|
224
|
+
include_context: bool = True,
|
|
225
|
+
*,
|
|
226
|
+
sanitize: bool = False,
|
|
227
|
+
autogenerate_capital_annotations: bool = True,
|
|
228
|
+
factor_out_shared_properties: bool = True) -> str:
|
|
215
229
|
"""
|
|
216
230
|
Serializes the MMIF object to a JSON string.
|
|
217
231
|
|
|
232
|
+
:param pretty: If True, returns the string with indentation.
|
|
233
|
+
:param include_context: If False, excludes contextual attributes (e.g.
|
|
234
|
+
runtime timestamps) from serialization.
|
|
218
235
|
:param sanitize: If True, performs some sanitization of before returning
|
|
219
236
|
the JSON string. See :meth:`sanitize` for details.
|
|
220
237
|
:param autogenerate_capital_annotations: If True, automatically convert
|
|
221
238
|
any "pending" temporary properties from `Document` objects to
|
|
222
239
|
`Annotation` objects. See :meth:`generate_capital_annotations` for
|
|
223
240
|
details.
|
|
224
|
-
:param
|
|
225
|
-
|
|
241
|
+
:param factor_out_shared_properties: If True, lift properties shared
|
|
242
|
+
(same value) across all annotations of a type in a view into the
|
|
243
|
+
view's ``contains`` metadata. See
|
|
244
|
+
:meth:`factor_out_shared_properties` for details. Independent of
|
|
245
|
+
``autogenerate_capital_annotations``, but runs after it (so
|
|
246
|
+
freshly-materialized pending properties are included) when both are
|
|
247
|
+
enabled.
|
|
226
248
|
:return: JSON string of the MMIF object.
|
|
227
249
|
"""
|
|
228
250
|
if autogenerate_capital_annotations:
|
|
229
251
|
self.generate_capital_annotations()
|
|
252
|
+
if factor_out_shared_properties:
|
|
253
|
+
self.factor_out_shared_properties()
|
|
230
254
|
# sanitization should be done after `Annotation` annotations are generated
|
|
231
255
|
if sanitize:
|
|
232
256
|
self.sanitize()
|
|
233
|
-
return super().serialize(
|
|
257
|
+
return super().serialize(pretty=pretty, include_context=include_context)
|
|
234
258
|
|
|
235
259
|
def _deserialize(self, input_dict: dict) -> None:
|
|
236
260
|
"""
|
|
@@ -252,18 +276,11 @@ class Mmif(MmifObject):
|
|
|
252
276
|
super()._deserialize(input_dict)
|
|
253
277
|
for view in self.views:
|
|
254
278
|
view._parent_mmif = self
|
|
255
|
-
# this dict will be populated with properties
|
|
256
|
-
# that are not encoded in individual annotations objects themselves
|
|
257
|
-
extrinsic_props = defaultdict(dict)
|
|
258
|
-
for at_type, type_lv_props in view.metadata.contains.items():
|
|
259
|
-
for prop_key, prop_value in type_lv_props.items():
|
|
260
|
-
extrinsic_props[at_type][prop_key] = prop_value
|
|
261
279
|
for ann in view.get_annotations():
|
|
262
280
|
## for "capital" Annotation properties
|
|
263
|
-
# first
|
|
264
|
-
# as "ephemeral" properties
|
|
265
|
-
|
|
266
|
-
ann._props_ephemeral[prop_key] = prop_value
|
|
281
|
+
# first, distribute view-level defaults from `contains` metadata
|
|
282
|
+
# to the Annotation objects as "ephemeral" properties
|
|
283
|
+
ann._add_ephemeral_defaults(view.metadata.contains.get(ann.at_type, {}))
|
|
267
284
|
# then, do the same to associated Document objects. Note that,
|
|
268
285
|
# in a view, it is guaranteed that all Annotation objects are not duplicates
|
|
269
286
|
if ann.at_type == AnnotationTypes.Annotation:
|
|
@@ -297,7 +314,7 @@ class Mmif(MmifObject):
|
|
|
297
314
|
f"Alignment {alignment_ann.id} has `source` and `target` properties that do not point to Annotation objects.",
|
|
298
315
|
RuntimeWarning)
|
|
299
316
|
## caching alignments
|
|
300
|
-
if all(map(lambda x: x in alignment_ann
|
|
317
|
+
if all(map(lambda x: x in alignment_ann, ('source', 'target'))):
|
|
301
318
|
try:
|
|
302
319
|
source_ann = self.__getitem__(alignment_ann.get('source'))
|
|
303
320
|
target_ann = self.__getitem__(alignment_ann.get('target'))
|
|
@@ -383,15 +400,51 @@ class Mmif(MmifObject):
|
|
|
383
400
|
if view_to_write.metadata.app == current_app and doc_id in view_to_write.annotations:
|
|
384
401
|
view_to_write[doc_id].properties.update(props)
|
|
385
402
|
else:
|
|
386
|
-
|
|
387
|
-
# if there's only one document, we can record the doc_id in the contains metadata
|
|
388
|
-
view_to_write.metadata.new_contain(AnnotationTypes.Annotation, document=doc_id)
|
|
389
|
-
props.pop('document', None)
|
|
390
|
-
else:
|
|
391
|
-
# otherwise, doc_id needs to be recorded in the annotation property
|
|
392
|
-
props['document'] = doc_id
|
|
403
|
+
props['document'] = doc_id
|
|
393
404
|
view_to_write.new_annotation(AnnotationTypes.Annotation, **props)
|
|
394
405
|
|
|
406
|
+
|
|
407
|
+
def factor_out_shared_properties(self):
|
|
408
|
+
"""
|
|
409
|
+
Factor properties that are shared (identical value) across all
|
|
410
|
+
annotations of a type within a view up into that view's ``contains``
|
|
411
|
+
metadata, per the MMIF spec recommendation to prefer view-level
|
|
412
|
+
defaults for properties shared among annotations of a type.
|
|
413
|
+
|
|
414
|
+
A property is factored when it is present, with the same value, on
|
|
415
|
+
**every** annotation of a given ``@type`` in the view, there are at
|
|
416
|
+
least two such annotations (so the value is genuinely shared), and the
|
|
417
|
+
key is not in the :attr:`_NON_FACTORABLE_PROPS` protection list.
|
|
418
|
+
Factored values are mirrored into each annotation's ephemeral props at
|
|
419
|
+
deserialization time, so in-memory access is unchanged, while the
|
|
420
|
+
serialized output carries the value once, in ``contains``.
|
|
421
|
+
"""
|
|
422
|
+
for view in self.views:
|
|
423
|
+
by_type = defaultdict(list)
|
|
424
|
+
for ann in view.get_annotations():
|
|
425
|
+
by_type[ann.at_type].append(ann)
|
|
426
|
+
for at_type, anns in by_type.items():
|
|
427
|
+
if len(anns) < 2:
|
|
428
|
+
continue
|
|
429
|
+
shared = {}
|
|
430
|
+
for key in list(anns[0].properties.keys()):
|
|
431
|
+
if key in self._NON_FACTORABLE_PROPS:
|
|
432
|
+
continue
|
|
433
|
+
value = anns[0].properties[key]
|
|
434
|
+
if all(key in a.properties and a.properties[key] == value
|
|
435
|
+
for a in anns):
|
|
436
|
+
shared[key] = value
|
|
437
|
+
if not shared:
|
|
438
|
+
continue
|
|
439
|
+
if at_type not in view.metadata.contains:
|
|
440
|
+
view.metadata.new_contain(at_type)
|
|
441
|
+
contain = view.metadata.contains[at_type]
|
|
442
|
+
for key, value in shared.items():
|
|
443
|
+
contain[key] = value
|
|
444
|
+
for a in anns:
|
|
445
|
+
del a.properties[key]
|
|
446
|
+
a._props_ephemeral[key] = value
|
|
447
|
+
|
|
395
448
|
def sanitize(self):
|
|
396
449
|
"""
|
|
397
450
|
Sanitizes a Mmif object by running some safeguards.
|
|
@@ -17,7 +17,7 @@ from datetime import datetime
|
|
|
17
17
|
from typing import Dict, Union, Optional, Generator, List, cast
|
|
18
18
|
|
|
19
19
|
from mmif import DocumentTypes, AnnotationTypes, ThingTypesBase, ClamsTypesBase
|
|
20
|
-
from mmif.serialize.annotation import Annotation, Document
|
|
20
|
+
from mmif.serialize.annotation import Annotation, Document, canonicalize_prop_name
|
|
21
21
|
from mmif.serialize.model import PRMTV_TYPES, MmifObject, DataList, DataDict
|
|
22
22
|
from mmif.vocabulary.base_types import AnnotationTypesBase
|
|
23
23
|
|
|
@@ -197,8 +197,6 @@ class View(MmifObject):
|
|
|
197
197
|
self._set_ann_id(new_annotation, aid)
|
|
198
198
|
for propk, propv in properties.items():
|
|
199
199
|
new_annotation.add_property(propk, propv)
|
|
200
|
-
for propk, propv in self.metadata.contains.get(at_type, {}).items():
|
|
201
|
-
new_annotation._props_ephemeral[propk] = propv
|
|
202
200
|
return self.add_annotation(new_annotation, overwrite)
|
|
203
201
|
|
|
204
202
|
def add_annotation(self, annotation: 'Annotation', overwrite=False) -> 'Annotation':
|
|
@@ -222,6 +220,7 @@ class View(MmifObject):
|
|
|
222
220
|
annotation.id = f"{self.id}{self.id_delimiter}{annotation.id}"
|
|
223
221
|
self.annotations.append(annotation, overwrite)
|
|
224
222
|
self.new_contain(annotation.at_type)
|
|
223
|
+
annotation._add_ephemeral_defaults(self.metadata.contains.get(annotation.at_type, {}))
|
|
225
224
|
if annotation.at_type == AnnotationTypes.Alignment:
|
|
226
225
|
self._parent_mmif._cache_alignment(annotation)
|
|
227
226
|
return annotation
|
|
@@ -280,13 +279,14 @@ class View(MmifObject):
|
|
|
280
279
|
must match. Note that annotation type metadata are specified in the `contains` view metadata, not in individual \
|
|
281
280
|
annotation objects.
|
|
282
281
|
"""
|
|
283
|
-
def prop_check(k, v,
|
|
284
|
-
|
|
282
|
+
def prop_check(k, v, annotation, at_type_metadata):
|
|
283
|
+
k = canonicalize_prop_name(annotation.at_type, k)
|
|
284
|
+
return any(k in prop and prop[k] == v for prop in (annotation.properties, at_type_metadata))
|
|
285
285
|
|
|
286
286
|
for annotation in self.annotations:
|
|
287
287
|
at_type_metadata = self.metadata.contains.get(annotation.at_type, {})
|
|
288
288
|
if not at_type or (at_type and annotation.at_type == at_type):
|
|
289
|
-
if all(map(lambda kv: prop_check(kv[0], kv[1], annotation
|
|
289
|
+
if all(map(lambda kv: prop_check(kv[0], kv[1], annotation, at_type_metadata),
|
|
290
290
|
properties.items())):
|
|
291
291
|
yield annotation
|
|
292
292
|
|
|
@@ -485,7 +485,7 @@ class ViewMetadata(MmifObject):
|
|
|
485
485
|
at_type = ThingTypesBase.from_str(at_type)
|
|
486
486
|
|
|
487
487
|
if at_type not in self.contains:
|
|
488
|
-
new_contain = Contain(contains_metadata)
|
|
488
|
+
new_contain = Contain._for_type(at_type, contains_metadata)
|
|
489
489
|
self.add_contain(new_contain, at_type)
|
|
490
490
|
return new_contain
|
|
491
491
|
|
|
@@ -563,6 +563,15 @@ class Contain(DataDict[str, str]):
|
|
|
563
563
|
annotation type in the ``contains`` metadata of a MMIF view.
|
|
564
564
|
"""
|
|
565
565
|
|
|
566
|
+
@classmethod
|
|
567
|
+
def _for_type(cls, at_type: ThingTypesBase, contains_metadata: dict) -> 'Contain':
|
|
568
|
+
"""
|
|
569
|
+
Builds a ``Contain`` for the given ``@type``, with property names
|
|
570
|
+
rewritten to their canonical spellings (see
|
|
571
|
+
:func:`mmif.serialize.annotation.canonicalize_prop_name`).
|
|
572
|
+
"""
|
|
573
|
+
return cls({canonicalize_prop_name(at_type, k): v for k, v in contains_metadata.items()})
|
|
574
|
+
|
|
566
575
|
|
|
567
576
|
class AnnotationsList(DataList[Union[Annotation, Document]]):
|
|
568
577
|
"""
|
|
@@ -608,7 +617,7 @@ class ContainsDict(DataDict[ThingTypesBase, Contain]):
|
|
|
608
617
|
for key, value in input_dict.items():
|
|
609
618
|
if isinstance(key, str):
|
|
610
619
|
key = ThingTypesBase.from_str(key)
|
|
611
|
-
self._items[key] = Contain(value)
|
|
620
|
+
self._items[key] = Contain._for_type(key, value)
|
|
612
621
|
|
|
613
622
|
def update(self, other: Union[dict, 'ContainsDict'], overwrite=False):
|
|
614
623
|
for k, v in other.items():
|