mmif-python 1.5.3__tar.gz → 1.6.0__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.3 → mmif_python-1.6.0}/CHANGELOG.md +27 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/PKG-INFO +2 -2
- {mmif_python-1.5.3 → mmif_python-1.6.0}/documentation/introduction.rst +2 -2
- {mmif_python-1.5.3 → mmif_python-1.6.0}/documentation/target-versions.csv +2 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/serialize/annotation.py +131 -65
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/serialize/mmif.py +129 -33
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/serialize/model.py +19 -1
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/serialize/view.py +26 -8
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/cli/rewind.py +11 -6
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/summarizer/nodes.py +64 -25
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/summarizer/summary.py +3 -3
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/video_document_helper.py +309 -40
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/workflow_helper.py +44 -28
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif_python.egg-info/PKG-INFO +2 -2
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif_python.egg-info/requires.txt +1 -1
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif_python.egg-info/scm_file_list.json +64 -64
- mmif_python-1.6.0/mmif_python.egg-info/scm_version.json +8 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/pyproject.toml +1 -1
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/test_serialize.py +371 -68
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/test_utils.py +597 -60
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/test_utils_cli.py +23 -0
- mmif_python-1.5.3/mmif_python.egg-info/scm_version.json +0 -8
- {mmif_python-1.5.3 → mmif_python-1.6.0}/.github/workflows/codecov.yml +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/.github/workflows/issue-assign.yml +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/.github/workflows/issue-close.yml +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/.github/workflows/issue-infra-project.yml +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/.github/workflows/publish.yml +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/.github/workflows/release-check.yml +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/.gitignore +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/CONTRIBUTING.md +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/LICENSE +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/README.md +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/build-tools/build.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/build-tools/clean.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/build-tools/docs.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/build-tools/prep_release.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/build-tools/publish.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/build-tools/test.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/documentation/_mmif_example_builder/__init__.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/documentation/_templates/versions.html +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/documentation/cli.rst +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/documentation/conf.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/documentation/consumer-tutorial.rst +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/documentation/index.rst +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/documentation/plugins.rst +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/documentation/summarizer.rst +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/documentation/target-versions.rst +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/__init__.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/res/__init__.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/res/mmif.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/serialize/__init__.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/__init__.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/cli/__init__.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/cli/describe.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/cli/source.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/cli/summarize.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/sequence_helper.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/summarizer/__init__.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/summarizer/config.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/summarizer/graph.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/summarizer/utils.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/text_document_helper.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/utils/timeunit_helper.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/ver/__init__.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/vocabulary/__init__.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/vocabulary/annotation_types.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/vocabulary/base_types.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif/vocabulary/document_types.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif_docloc_http/__init__.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif_python.egg-info/SOURCES.txt +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif_python.egg-info/dependency_links.txt +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif_python.egg-info/entry_points.txt +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/mmif_python.egg-info/top_level.txt +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/setup.cfg +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/black-2997fps.mp4 +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/black-2997fps.webm +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/1.0.5-old-shortid.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/README.md +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/bars-tones-slates/raw.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/east-tesseract-typing/raw.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/everything/raw.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/examples.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/others/README.md +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/others/image-box-ocr-timex.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/others/image-box-ocr.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/others/image-box.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/others/image.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/others/slate-detect.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/others/slate-reco.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/others/video-demux.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/others/video-transcript-demux-fa.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/others/video-transcript-demux-fa.short.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/others/video-transcript-demux.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/others/video-transcript-sentencesplit.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/others/video-transcript.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/others/video.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/segmenter-kaldi-ner/raw.json +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif-examples/swt-1.0.mmif +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/mmif_examples.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/test_specver.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/test_vocab.py +0 -0
- {mmif_python-1.5.3 → mmif_python-1.6.0}/tests/testsrc-2997fps-ptsoffset.mp4 +0 -0
|
@@ -1,4 +1,31 @@
|
|
|
1
1
|
|
|
2
|
+
## releasing 1.5.4 (2026-10-05)
|
|
3
|
+
### Overview
|
|
4
|
+
|
|
5
|
+
This patch fixes image extraction from truncated or otherwise incomplete video files, and the handling of deprecated annotation property names.
|
|
6
|
+
|
|
7
|
+
### Changes
|
|
8
|
+
|
|
9
|
+
* `video_document_helper`: image extraction no longer fails or returns wrong images on incomplete video files. Timepoints with no image now come back as `None`, with a warning that tells how to check the file (https://github.com/clamsproject/mmif-python/issues/400).
|
|
10
|
+
* *Note: apps that process every returned image must handle `None`.*
|
|
11
|
+
* `av` is now pinned to `>= 15` for `mmif-python[cv]`
|
|
12
|
+
* Deprecated property names (e.g., `frameType` for `label`) work again for values stored in a view's `contains` metadata, including in `mmif summarize` output (https://github.com/clamsproject/mmif-python/issues/401).
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
## releasing 1.5.3 (2026-07-08)
|
|
18
|
+
### Overview
|
|
19
|
+
This patch includes improvements in automatic conversion between contains-metadata properties and annotation properties (#396)
|
|
20
|
+
|
|
21
|
+
### Additions
|
|
22
|
+
* 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)
|
|
23
|
+
|
|
24
|
+
### Changes
|
|
25
|
+
* 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)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
2
29
|
## releasing 1.5.2 (2026-07-07)
|
|
3
30
|
### Overview
|
|
4
31
|
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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mmif-python
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.6.0
|
|
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
|
|
@@ -94,6 +94,6 @@ For a full list of available helper methods, please refer to the API documentati
|
|
|
94
94
|
MMIF usage in CLAMS Workflows
|
|
95
95
|
-----------------------------
|
|
96
96
|
|
|
97
|
-
In the context of CLAMS, a **Workflow** refers to the sequence of CLAMS applications that have been executed to generate the views and annotations within a MMIF file.
|
|
97
|
+
In the context of CLAMS, a **Workflow** refers to the sequence of CLAMS applications that have been executed to generate the views and annotations within a MMIF file. Each step of a workflow is one *app execution*: the views produced by a single run of an app, which share the same ``app`` and ``timestamp`` values in their metadata. A step counts whether its views hold annotations, only warnings, or an error, so the workflow records every app that ran, including ones that failed or found nothing to annotate.
|
|
98
98
|
|
|
99
|
-
When using the ``mmif-python`` SDK, a unique identifier for a workflow (``workflowId``) is calculated based on the applications involved. This identifier is constructed by concatenating the application name, version, and a hash of the runtime parameters for each step in the sequence. This ensures that the identifier uniquely represents not just the apps used, but their specific configurations, aiding in reproducibility.
|
|
99
|
+
When using the ``mmif-python`` SDK, a unique identifier for a workflow (``workflowId``) is calculated based on the applications involved. This identifier is constructed by concatenating a prefix that describes the source documents, followed by the application name, version, and a hash of the runtime parameters for each step in the sequence. This ensures that the identifier uniquely represents not just the apps used, but their specific configurations, aiding in reproducibility. The ``mmif rewind`` command in app mode counts the same steps, so rewinding N apps from a MMIF yields the ``workflowId`` with its last N steps removed.
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
"``mmif-python`` version","Target MMIF Specification"
|
|
2
|
+
`1.6.0 <https://pypi.org/project/mmif-python/1.6.0/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
|
|
3
|
+
`1.5.4 <https://pypi.org/project/mmif-python/1.5.4/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
|
|
2
4
|
`1.5.3 <https://pypi.org/project/mmif-python/1.5.3/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
|
|
3
5
|
`1.5.2 <https://pypi.org/project/mmif-python/1.5.2/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
|
|
4
6
|
`1.5.1 <https://pypi.org/project/mmif-python/1.5.1/>`__,`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,14 @@ 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
|
-
|
|
370
|
+
self._props_original: DocumentProperties = DocumentProperties(None, self)
|
|
371
|
+
# pending props are never cleared: every `Mmif.serialize()` call
|
|
372
|
+
# converts them into `Annotation`s again, and
|
|
373
|
+
# `Mmif.generate_capital_annotations()` relies on its duplicate check
|
|
374
|
+
# to skip values already written. Calling it more than once (e.g.,
|
|
375
|
+
# early in clams-python's `ClamsApp.annotate()`) is safe only while
|
|
376
|
+
# that check holds.
|
|
377
|
+
self._props_pending: AnnotationProperties = AnnotationProperties(None, self)
|
|
361
378
|
self.reserved_names.update(('_props_original', '_props_pending'))
|
|
362
379
|
|
|
363
380
|
self._type: Union[ThingTypesBase, DocumentTypesBase] = ThingTypesBase('')
|
|
@@ -578,6 +595,7 @@ class AnnotationProperties(MmifObject, MutableMapping[str, T]):
|
|
|
578
595
|
"""
|
|
579
596
|
|
|
580
597
|
def __delitem__(self, key: str) -> None:
|
|
598
|
+
key = self._canonical(key)
|
|
581
599
|
frm = None
|
|
582
600
|
for k in self.__iter__():
|
|
583
601
|
if k == key:
|
|
@@ -593,36 +611,83 @@ class AnnotationProperties(MmifObject, MutableMapping[str, T]):
|
|
|
593
611
|
del frm[key]
|
|
594
612
|
else:
|
|
595
613
|
raise KeyError(f'Key "{key}" not found.')
|
|
596
|
-
|
|
614
|
+
|
|
597
615
|
def __iter__(self) -> Iterator[str]:
|
|
598
616
|
"""
|
|
599
|
-
``__iter__`` on Mapping should basically work as ``keys()`` method
|
|
617
|
+
``__iter__`` on Mapping should basically work as ``keys()`` method
|
|
600
618
|
of vanilla dict.
|
|
601
619
|
"""
|
|
602
620
|
for key in itertools.chain(self._named_attributes(), self._unnamed_attributes):
|
|
603
621
|
yield key
|
|
604
|
-
|
|
622
|
+
|
|
605
623
|
def __getitem__(self, key):
|
|
606
624
|
"""
|
|
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.
|
|
625
|
+
Parent MmifObject class has a __getitem__ method that checks if
|
|
626
|
+
the value is empty when asked for an unnamed attribute. But for
|
|
627
|
+
AnnotationProperties, any arbitrary property that's added
|
|
628
|
+
explicitly by the user (developer) should not be ignored and
|
|
629
|
+
returned even the value is empty.
|
|
612
630
|
"""
|
|
631
|
+
key = self._canonical(key)
|
|
613
632
|
if key in self._named_attributes():
|
|
614
633
|
return self.__dict__[key]
|
|
615
634
|
else:
|
|
616
635
|
return self._unnamed_attributes[key]
|
|
617
636
|
|
|
618
|
-
def
|
|
637
|
+
def __setitem__(self, key: str, value) -> None:
|
|
638
|
+
"""
|
|
639
|
+
Sets a property. A deprecated spelling of an aliased property (e.g.
|
|
640
|
+
``frameType``) is stored under its canonical name (``label``), with
|
|
641
|
+
a :class:`DeprecationWarning`.
|
|
642
|
+
"""
|
|
643
|
+
canonical = self._canonical(key)
|
|
644
|
+
if canonical != key:
|
|
645
|
+
warnings.warn(f'"{key}" is a deprecated alias of "{canonical}", storing the value as "{canonical}".',
|
|
646
|
+
DeprecationWarning)
|
|
647
|
+
super().__setitem__(canonical, value)
|
|
648
|
+
|
|
649
|
+
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None,
|
|
650
|
+
parent: Optional['Annotation'] = None, *_) -> None:
|
|
619
651
|
self.id: str = ''
|
|
620
652
|
# any individual at_type (subclassing this class) can have its own set of required attributes
|
|
621
653
|
self._required_attributes = ["id"]
|
|
622
654
|
# allowing additional attributes for arbitrary annotation properties
|
|
623
655
|
self._unnamed_attributes = {}
|
|
656
|
+
# the Annotation these properties belong to; its `@type` decides which
|
|
657
|
+
# property names are aliases of each other (see `canonicalize_prop_name`)
|
|
658
|
+
self._parent_annotation = parent
|
|
659
|
+
self.reserved_names.add('_parent_annotation')
|
|
624
660
|
super().__init__(mmif_obj)
|
|
625
661
|
|
|
662
|
+
def _canonical(self, key: str) -> str:
|
|
663
|
+
if self._parent_annotation is None:
|
|
664
|
+
return key
|
|
665
|
+
return canonicalize_prop_name(self._parent_annotation._type, key)
|
|
666
|
+
|
|
667
|
+
def _deserialize(self, input_dict: dict) -> None:
|
|
668
|
+
"""
|
|
669
|
+
Deserializes the properties, rewriting deprecated spellings to their
|
|
670
|
+
canonical names so that there is one key per property. When two
|
|
671
|
+
spellings of the same property are present, the canonical one wins
|
|
672
|
+
(or the first one seen, when neither is canonical), with a warning.
|
|
673
|
+
"""
|
|
674
|
+
canonical_dict = {}
|
|
675
|
+
# canonical name -> the deprecated spelling it was read from
|
|
676
|
+
from_alias = {}
|
|
677
|
+
for key, value in input_dict.items():
|
|
678
|
+
canonical = self._canonical(key)
|
|
679
|
+
if canonical in canonical_dict and (canonical != key or canonical in from_alias):
|
|
680
|
+
alias = key if canonical != key else from_alias[canonical]
|
|
681
|
+
warnings.warn(f'Both "{alias}" and "{canonical}" are in the properties of '
|
|
682
|
+
f'"{input_dict.get("id", "")}", but "{alias}" is an alias of "{canonical}". '
|
|
683
|
+
f'Keeping the value of "{canonical}".', UserWarning)
|
|
684
|
+
if canonical != key:
|
|
685
|
+
continue
|
|
686
|
+
canonical_dict[canonical] = value
|
|
687
|
+
if canonical != key:
|
|
688
|
+
from_alias[canonical] = key
|
|
689
|
+
super()._deserialize(canonical_dict)
|
|
690
|
+
|
|
626
691
|
|
|
627
692
|
class DocumentProperties(AnnotationProperties):
|
|
628
693
|
"""
|
|
@@ -632,7 +697,8 @@ class DocumentProperties(AnnotationProperties):
|
|
|
632
697
|
:param mmif_obj: the JSON data that defines the properties
|
|
633
698
|
"""
|
|
634
699
|
|
|
635
|
-
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None,
|
|
700
|
+
def __init__(self, mmif_obj: Optional[Union[bytes, str, dict]] = None,
|
|
701
|
+
parent: Optional['Annotation'] = None, *_) -> None:
|
|
636
702
|
self.mime: str = ''
|
|
637
703
|
# note the trailing underscore here. I wanted to use the name `location`
|
|
638
704
|
# for @property in this class and `Document` class, so had to use a diff
|
|
@@ -643,9 +709,9 @@ class DocumentProperties(AnnotationProperties):
|
|
|
643
709
|
self._attribute_classes = {'text': Text}
|
|
644
710
|
# in theory, either `location` or `text` should appear in a `document`
|
|
645
711
|
# 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)
|
|
712
|
+
# for `oneOf` requirement
|
|
713
|
+
# see MmifObject::_required_attributes in model.py
|
|
714
|
+
super().__init__(mmif_obj, parent)
|
|
649
715
|
|
|
650
716
|
def _deserialize(self, input_dict: dict) -> None:
|
|
651
717
|
if "location" in input_dict:
|
|
@@ -15,7 +15,8 @@ import math
|
|
|
15
15
|
import warnings
|
|
16
16
|
from collections import defaultdict
|
|
17
17
|
from datetime import datetime, timezone
|
|
18
|
-
from typing import Any, List, Union, Optional, Dict, cast, Iterator
|
|
18
|
+
from typing import Any, List, Union, Optional, Dict, cast, Iterable, Iterator
|
|
19
|
+
from urllib.parse import urlparse
|
|
19
20
|
|
|
20
21
|
import jsonschema.validators
|
|
21
22
|
|
|
@@ -28,6 +29,10 @@ from mmif.vocabulary import AnnotationTypes, DocumentTypesBase
|
|
|
28
29
|
|
|
29
30
|
__all__ = ['Mmif']
|
|
30
31
|
|
|
32
|
+
# base URL of first-party CLAMS app identifiers
|
|
33
|
+
# (e.g., ``http://apps.clams.ai/swt-detection/v8.6``)
|
|
34
|
+
app_directory_baseurl = "http://apps.clams.ai"
|
|
35
|
+
|
|
31
36
|
|
|
32
37
|
class MmifMetadata(MmifObject):
|
|
33
38
|
"""
|
|
@@ -122,11 +127,15 @@ class ViewsList(DataList[View]):
|
|
|
122
127
|
|
|
123
128
|
def get_last_contentful_view(self) -> Optional[View]:
|
|
124
129
|
"""
|
|
125
|
-
Returns the last view that is contentful, i.e., has
|
|
130
|
+
Returns the last view that is contentful, i.e., has at least one
|
|
131
|
+
annotation (see :meth:`~mmif.serialize.view.View.has_content`).
|
|
132
|
+
Views with no annotations, including error and warning views, are
|
|
133
|
+
skipped.
|
|
126
134
|
"""
|
|
127
135
|
for view in reversed(self._items.values()):
|
|
128
|
-
if
|
|
136
|
+
if view.has_content():
|
|
129
137
|
return view
|
|
138
|
+
return None
|
|
130
139
|
|
|
131
140
|
def get_last_view(self) -> Optional[View]:
|
|
132
141
|
"""
|
|
@@ -276,25 +285,11 @@ class Mmif(MmifObject):
|
|
|
276
285
|
super()._deserialize(input_dict)
|
|
277
286
|
for view in self.views:
|
|
278
287
|
view._parent_mmif = self
|
|
279
|
-
# this dict will be populated with properties
|
|
280
|
-
# that are not encoded in individual annotations objects themselves
|
|
281
|
-
extrinsic_props = defaultdict(dict)
|
|
282
|
-
for at_type, type_lv_props in view.metadata.contains.items():
|
|
283
|
-
for prop_key, prop_value in type_lv_props.items():
|
|
284
|
-
extrinsic_props[at_type][prop_key] = prop_value
|
|
285
288
|
for ann in view.get_annotations():
|
|
286
289
|
## for "capital" Annotation properties
|
|
287
|
-
# first
|
|
288
|
-
# as "ephemeral" properties
|
|
289
|
-
|
|
290
|
-
# in `_props_ephemeral` -- namely an alias of a property set on the
|
|
291
|
-
# annotation itself (put there by `_add_prop_aliases` during the
|
|
292
|
-
# annotation's own deserialization, which runs earlier). Per the
|
|
293
|
-
# spec, the annotation-level value takes precedence over the
|
|
294
|
-
# view-level default.
|
|
295
|
-
for prop_key, prop_value in extrinsic_props[ann.at_type].items():
|
|
296
|
-
if prop_key not in ann._props_ephemeral:
|
|
297
|
-
ann._props_ephemeral[prop_key] = prop_value
|
|
290
|
+
# first, distribute view-level defaults from `contains` metadata
|
|
291
|
+
# to the Annotation objects as "ephemeral" properties
|
|
292
|
+
ann._add_ephemeral_defaults(view.metadata.contains.get(ann.at_type, {}))
|
|
298
293
|
# then, do the same to associated Document objects. Note that,
|
|
299
294
|
# in a view, it is guaranteed that all Annotation objects are not duplicates
|
|
300
295
|
if ann.at_type == AnnotationTypes.Annotation:
|
|
@@ -350,7 +345,9 @@ class Mmif(MmifObject):
|
|
|
350
345
|
behind this behavior and discussion.
|
|
351
346
|
"""
|
|
352
347
|
# this view will be the default kitchen sink for all generated annotations.
|
|
353
|
-
|
|
348
|
+
# this can be an empty view, from an app that only adds document properties
|
|
349
|
+
last_view = next((v for v in reversed(self.views)
|
|
350
|
+
if not v.has_error() and not v.has_warnings()), None)
|
|
354
351
|
|
|
355
352
|
# proceed only when there's at least one view
|
|
356
353
|
if last_view:
|
|
@@ -370,18 +367,12 @@ class Mmif(MmifObject):
|
|
|
370
367
|
# new properties to record in the current serialization call
|
|
371
368
|
anns_to_write = defaultdict(dict)
|
|
372
369
|
for view in self.views:
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
# note that in the input MMIF that generated with old implementation,
|
|
380
|
-
# this value can be in the "short" form, i.e., without view ID prefix
|
|
381
|
-
# however, after deserialization, all document IDs should have the
|
|
382
|
-
# view ID prefix, at least within in-memory representation.
|
|
383
|
-
doc_id = ann.get_property('document')
|
|
384
|
-
existing_anns[doc_id].update(ann.properties.items())
|
|
370
|
+
for ann in view.get_annotations(AnnotationTypes.Annotation):
|
|
371
|
+
# view-level `contains` defaults are exposed on each
|
|
372
|
+
# annotation as ephemeral properties; in MMIF made by old
|
|
373
|
+
# SDKs, the id may be in the short form (no view prefix)
|
|
374
|
+
existing_anns[ann.get_property('document')].update(
|
|
375
|
+
ann.properties.items())
|
|
385
376
|
for doc in view.get_documents():
|
|
386
377
|
anns_to_write[doc.id].update(doc._props_pending.items())
|
|
387
378
|
for doc in self.documents:
|
|
@@ -777,6 +768,111 @@ class Mmif(MmifObject):
|
|
|
777
768
|
return view
|
|
778
769
|
return None
|
|
779
770
|
|
|
771
|
+
@staticmethod
|
|
772
|
+
def _full_app_id(app_id: str) -> str:
|
|
773
|
+
"""
|
|
774
|
+
Expands a first-party app ID shorthand (e.g.,
|
|
775
|
+
``swt-detection/v8.6``) into a full URI under
|
|
776
|
+
:data:`app_directory_baseurl`. An ``http(s)`` URL is returned as is.
|
|
777
|
+
"""
|
|
778
|
+
if urlparse(app_id).scheme in ('http', 'https'):
|
|
779
|
+
return app_id
|
|
780
|
+
return f"{app_directory_baseurl}/{app_id.strip('/')}"
|
|
781
|
+
|
|
782
|
+
def get_all_views_by_app(self, *app_ids: str) -> List[View]:
|
|
783
|
+
"""
|
|
784
|
+
Returns the list of all views produced by any of the given apps.
|
|
785
|
+
|
|
786
|
+
Each app ID is either a full app URI (required for third-party
|
|
787
|
+
apps) or a first-party shorthand such as ``swt-detection/v8.6``,
|
|
788
|
+
which is expanded under :data:`app_directory_baseurl`. Matching is
|
|
789
|
+
exact: an ID without a version matches no view, and there are no
|
|
790
|
+
wildcards, so list every version to cover a version range. To group
|
|
791
|
+
views into app runs instead, see
|
|
792
|
+
:func:`mmif.utils.workflow_helper.group_views_by_app`.
|
|
793
|
+
|
|
794
|
+
:param app_ids: one or more app IDs; a view matches if it was
|
|
795
|
+
produced by any of them
|
|
796
|
+
:return: the list of matching views
|
|
797
|
+
"""
|
|
798
|
+
full_ids = {self._full_app_id(app_id) for app_id in app_ids}
|
|
799
|
+
return [view for view in self.views if view.metadata.app in full_ids]
|
|
800
|
+
|
|
801
|
+
get_views_by_app = get_all_views_by_app
|
|
802
|
+
|
|
803
|
+
def get_view_by_app(self, *app_ids: str) -> Optional[View]:
|
|
804
|
+
"""
|
|
805
|
+
Returns the last view appended that was produced by any of the given
|
|
806
|
+
apps. App IDs are matched as in :meth:`get_all_views_by_app`.
|
|
807
|
+
|
|
808
|
+
:param app_ids: one or more app IDs
|
|
809
|
+
:return: the view, or None if no view matches
|
|
810
|
+
"""
|
|
811
|
+
matches = self.get_all_views_by_app(*app_ids)
|
|
812
|
+
return matches[-1] if matches else None
|
|
813
|
+
|
|
814
|
+
def get_all_views_by_apptag(self, *tags: str) -> List[View]:
|
|
815
|
+
"""
|
|
816
|
+
Returns the list of all views whose ``appTags`` metadata contains
|
|
817
|
+
any of the given tags.
|
|
818
|
+
|
|
819
|
+
:param tags: one or more tags; a view matches if it carries any of
|
|
820
|
+
them
|
|
821
|
+
:return: the list of matching views
|
|
822
|
+
"""
|
|
823
|
+
return [view for view in self.views
|
|
824
|
+
if any(tag in view.metadata.appTags for tag in tags)]
|
|
825
|
+
|
|
826
|
+
get_views_by_apptag = get_all_views_by_apptag
|
|
827
|
+
|
|
828
|
+
def get_view_by_apptag(self, *tags: str) -> Optional[View]:
|
|
829
|
+
"""
|
|
830
|
+
Returns the last view appended whose ``appTags`` metadata contains
|
|
831
|
+
any of the given tags.
|
|
832
|
+
|
|
833
|
+
:param tags: one or more tags
|
|
834
|
+
:return: the view, or None if no view matches
|
|
835
|
+
"""
|
|
836
|
+
matches = self.get_all_views_by_apptag(*tags)
|
|
837
|
+
return matches[-1] if matches else None
|
|
838
|
+
|
|
839
|
+
def get_annotations_of_labels(
|
|
840
|
+
self, at_type: Union[ThingTypesBase, str],
|
|
841
|
+
include_labels: Iterable[str] = (),
|
|
842
|
+
exclude_labels: Iterable[str] = ()) -> Iterator[Annotation]:
|
|
843
|
+
"""
|
|
844
|
+
Finds annotations of a type across all views, filtered by their
|
|
845
|
+
``label`` property.
|
|
846
|
+
|
|
847
|
+
With both label lists empty (the default), every annotation of
|
|
848
|
+
``at_type`` is returned, including annotations that have no
|
|
849
|
+
``label``. A non-empty ``include_labels`` keeps only annotations
|
|
850
|
+
whose ``label`` is listed, so annotations without a ``label`` are
|
|
851
|
+
dropped. ``exclude_labels`` is applied after ``include_labels`` and
|
|
852
|
+
never drops annotations without a ``label``.
|
|
853
|
+
|
|
854
|
+
The ``label`` value is read with
|
|
855
|
+
:py:meth:`~mmif.serialize.annotation.Annotation.get_property`, so
|
|
856
|
+
the deprecated spellings of the property (``frameType`` on
|
|
857
|
+
``TimeFrame``, ``boxType`` on ``BoundingBox``) are honored.
|
|
858
|
+
|
|
859
|
+
:param at_type: the type of the annotations to look for
|
|
860
|
+
:param include_labels: labels to keep; empty means no restriction
|
|
861
|
+
:param exclude_labels: labels to leave out
|
|
862
|
+
:return: an iterator of matching annotations, in view order and
|
|
863
|
+
then in annotation order within each view
|
|
864
|
+
"""
|
|
865
|
+
include = tuple(include_labels)
|
|
866
|
+
exclude = tuple(exclude_labels)
|
|
867
|
+
for view in self.get_all_views_contain(at_type):
|
|
868
|
+
for ann in view.get_annotations(at_type):
|
|
869
|
+
label = ann.get_property('label')
|
|
870
|
+
if include and label not in include:
|
|
871
|
+
continue
|
|
872
|
+
if label is not None and label in exclude:
|
|
873
|
+
continue
|
|
874
|
+
yield ann
|
|
875
|
+
|
|
780
876
|
def _is_in_time_range(self, ann: Annotation, range_s: Union[int, float], range_e: Union[int, float]) -> bool:
|
|
781
877
|
"""
|
|
782
878
|
Checks if the annotation is anchored within the given time range. Any overlap is considered included.
|
|
@@ -134,13 +134,31 @@ class MmifObject(object):
|
|
|
134
134
|
"""
|
|
135
135
|
Method to set values in _unnamed_attributes.
|
|
136
136
|
|
|
137
|
+
A key that matches a declared (named) attribute is stored in that
|
|
138
|
+
attribute instead, with a warning, so that a key never lives in both
|
|
139
|
+
places. A leading ``@`` in the key is read as the ``_`` prefix of
|
|
140
|
+
the attribute name, following the JSON-LD mapping used in
|
|
141
|
+
serialization.
|
|
142
|
+
|
|
137
143
|
:param key: the attribute name
|
|
138
144
|
:param value: the desired value
|
|
139
145
|
:return: None
|
|
140
146
|
:raise: AttributeError if additional properties are disallowed by :func:`disallow_additional_properties`
|
|
147
|
+
:raise: KeyError if the key is a reserved name
|
|
141
148
|
"""
|
|
149
|
+
attr = f'_{key[1:]}' if key.startswith('@') else key
|
|
142
150
|
if self._unnamed_attributes is None:
|
|
143
|
-
|
|
151
|
+
hint = (f' "{key}" is a declared attribute; use obj["{attr}"] = value.'
|
|
152
|
+
if attr in self._named_attributes() else '')
|
|
153
|
+
raise AttributeError(f"Additional properties are disallowed by {self.__class__}.{hint}")
|
|
154
|
+
if key in self.reserved_names or attr in self.reserved_names:
|
|
155
|
+
raise KeyError(f"can't set a reserved name: {key}")
|
|
156
|
+
if attr in self._named_attributes():
|
|
157
|
+
warnings.warn(f'"{key}" is a declared attribute of {type(self).__name__}; '
|
|
158
|
+
f'storing it as the attribute, not as an additional property.',
|
|
159
|
+
stacklevel=2)
|
|
160
|
+
self[attr] = value
|
|
161
|
+
return
|
|
144
162
|
self._unnamed_attributes[key] = value # pytype: disable=unsupported-operands
|
|
145
163
|
|
|
146
164
|
def _named_attributes(self) -> Generator[str, None, None]:
|