mmif-python 1.5.0__tar.gz → 1.5.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. mmif_python-1.5.1/.github/workflows/release-check.yml +11 -0
  2. {mmif_python-1.5.0 → mmif_python-1.5.1}/CHANGELOG.md +25 -0
  3. {mmif_python-1.5.0 → mmif_python-1.5.1}/CONTRIBUTING.md +8 -1
  4. {mmif_python-1.5.0/mmif_python.egg-info → mmif_python-1.5.1}/PKG-INFO +1 -1
  5. mmif_python-1.5.1/build-tools/prep_release.py +91 -0
  6. {mmif_python-1.5.0 → mmif_python-1.5.1}/documentation/conf.py +0 -38
  7. mmif_python-1.5.1/documentation/target-versions.csv +52 -0
  8. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/serialize/annotation.py +50 -28
  9. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/video_document_helper.py +22 -6
  10. {mmif_python-1.5.0 → mmif_python-1.5.1/mmif_python.egg-info}/PKG-INFO +1 -1
  11. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif_python.egg-info/SOURCES.txt +3 -0
  12. mmif_python-1.5.1/tests/black-2997fps.webm +0 -0
  13. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/test_serialize.py +30 -1
  14. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/test_utils.py +20 -0
  15. mmif_python-1.5.0/documentation/target-versions.csv +0 -47
  16. {mmif_python-1.5.0 → mmif_python-1.5.1}/.github/workflows/codecov.yml +0 -0
  17. {mmif_python-1.5.0 → mmif_python-1.5.1}/.github/workflows/issue-assign.yml +0 -0
  18. {mmif_python-1.5.0 → mmif_python-1.5.1}/.github/workflows/issue-close.yml +0 -0
  19. {mmif_python-1.5.0 → mmif_python-1.5.1}/.github/workflows/issue-infra-project.yml +0 -0
  20. {mmif_python-1.5.0 → mmif_python-1.5.1}/.github/workflows/publish.yml +0 -0
  21. {mmif_python-1.5.0 → mmif_python-1.5.1}/.gitignore +0 -0
  22. {mmif_python-1.5.0 → mmif_python-1.5.1}/LICENSE +0 -0
  23. {mmif_python-1.5.0 → mmif_python-1.5.1}/README.md +0 -0
  24. {mmif_python-1.5.0 → mmif_python-1.5.1}/build-tools/build.py +0 -0
  25. {mmif_python-1.5.0 → mmif_python-1.5.1}/build-tools/clean.py +0 -0
  26. {mmif_python-1.5.0 → mmif_python-1.5.1}/build-tools/docs.py +0 -0
  27. {mmif_python-1.5.0 → mmif_python-1.5.1}/build-tools/publish.py +0 -0
  28. {mmif_python-1.5.0 → mmif_python-1.5.1}/build-tools/test.py +0 -0
  29. {mmif_python-1.5.0 → mmif_python-1.5.1}/documentation/_mmif_example_builder/__init__.py +0 -0
  30. {mmif_python-1.5.0 → mmif_python-1.5.1}/documentation/_templates/versions.html +0 -0
  31. {mmif_python-1.5.0 → mmif_python-1.5.1}/documentation/cli.rst +0 -0
  32. {mmif_python-1.5.0 → mmif_python-1.5.1}/documentation/consumer-tutorial.rst +0 -0
  33. {mmif_python-1.5.0 → mmif_python-1.5.1}/documentation/index.rst +0 -0
  34. {mmif_python-1.5.0 → mmif_python-1.5.1}/documentation/introduction.rst +0 -0
  35. {mmif_python-1.5.0 → mmif_python-1.5.1}/documentation/plugins.rst +0 -0
  36. {mmif_python-1.5.0 → mmif_python-1.5.1}/documentation/summarizer.rst +0 -0
  37. {mmif_python-1.5.0 → mmif_python-1.5.1}/documentation/target-versions.rst +0 -0
  38. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/__init__.py +0 -0
  39. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/res/__init__.py +0 -0
  40. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/res/mmif.json +0 -0
  41. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/serialize/__init__.py +0 -0
  42. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/serialize/mmif.py +0 -0
  43. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/serialize/model.py +0 -0
  44. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/serialize/view.py +0 -0
  45. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/__init__.py +0 -0
  46. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/cli/__init__.py +0 -0
  47. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/cli/describe.py +0 -0
  48. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/cli/rewind.py +0 -0
  49. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/cli/source.py +0 -0
  50. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/cli/summarize.py +0 -0
  51. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/sequence_helper.py +0 -0
  52. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/summarizer/__init__.py +0 -0
  53. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/summarizer/config.py +0 -0
  54. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/summarizer/graph.py +0 -0
  55. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/summarizer/nodes.py +0 -0
  56. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/summarizer/summary.py +0 -0
  57. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/summarizer/utils.py +0 -0
  58. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/text_document_helper.py +0 -0
  59. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/timeunit_helper.py +0 -0
  60. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/utils/workflow_helper.py +0 -0
  61. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/ver/__init__.py +0 -0
  62. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/vocabulary/__init__.py +0 -0
  63. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/vocabulary/annotation_types.py +0 -0
  64. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/vocabulary/base_types.py +0 -0
  65. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif/vocabulary/document_types.py +0 -0
  66. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif_docloc_http/__init__.py +0 -0
  67. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif_python.egg-info/dependency_links.txt +0 -0
  68. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif_python.egg-info/entry_points.txt +0 -0
  69. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif_python.egg-info/requires.txt +0 -0
  70. {mmif_python-1.5.0 → mmif_python-1.5.1}/mmif_python.egg-info/top_level.txt +0 -0
  71. {mmif_python-1.5.0 → mmif_python-1.5.1}/pyproject.toml +0 -0
  72. {mmif_python-1.5.0 → mmif_python-1.5.1}/setup.cfg +0 -0
  73. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/black-2997fps.mp4 +0 -0
  74. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/1.0.5-old-shortid.json +0 -0
  75. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/README.md +0 -0
  76. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/bars-tones-slates/raw.json +0 -0
  77. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/east-tesseract-typing/raw.json +0 -0
  78. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/everything/raw.json +0 -0
  79. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/examples.json +0 -0
  80. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/others/README.md +0 -0
  81. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/others/image-box-ocr-timex.json +0 -0
  82. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/others/image-box-ocr.json +0 -0
  83. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/others/image-box.json +0 -0
  84. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/others/image.json +0 -0
  85. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/others/slate-detect.json +0 -0
  86. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/others/slate-reco.json +0 -0
  87. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/others/video-demux.json +0 -0
  88. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/others/video-transcript-demux-fa.json +0 -0
  89. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/others/video-transcript-demux-fa.short.json +0 -0
  90. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/others/video-transcript-demux.json +0 -0
  91. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/others/video-transcript-sentencesplit.json +0 -0
  92. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/others/video-transcript.json +0 -0
  93. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/others/video.json +0 -0
  94. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/segmenter-kaldi-ner/raw.json +0 -0
  95. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif-examples/swt-1.0.mmif +0 -0
  96. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/mmif_examples.py +0 -0
  97. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/test_specver.py +0 -0
  98. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/test_utils_cli.py +0 -0
  99. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/test_vocab.py +0 -0
  100. {mmif_python-1.5.0 → mmif_python-1.5.1}/tests/testsrc-2997fps-ptsoffset.mp4 +0 -0
@@ -0,0 +1,11 @@
1
+ name: "🔖 Release PR check"
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [main]
6
+ types: [opened, synchronize, reopened, edited]
7
+
8
+ jobs:
9
+ release-check:
10
+ name: "🤙 Call SDK release-check workflow"
11
+ uses: clamsproject/.github/.github/workflows/sdk-release-check.yml@main
@@ -1,4 +1,29 @@
1
1
 
2
+ ## releasing 1.5.0 (2026-05-27)
3
+ ### Overview
4
+
5
+ Improves image-extraction API in `video_document_helper` for TP source tracking, restores the source-composition prefix in workflow identifiers, and fixes the versioned-docs publishing pipeline.
6
+
7
+ ### Additions
8
+
9
+ * `extract_images_by_count_with_sources` and `extract_images_by_mode_with_sources`: new public functions in `mmif.utils.video_document_helper` that return per-image source IDs (or sampled timepoints in ms when a fallback path is used), so downstream apps can anchor each extracted image to a specific point in the source media (https://github.com/clamsproject/mmif-python/pull/385, https://github.com/clamsproject/mmif-python/issues/384).
10
+
11
+ ### Changes
12
+
13
+ * `mmif.utils.video_document_helper`: renamed
14
+ * `extract_timepoints_as_images` --> `extract_images_from_timepoints`,
15
+ * `extract_target_frames` --> `extract_images_by_count_with_sources` (alongside new bare `extract_images_by_count`), and
16
+ * `extract_frames_by_mode` --> `extract_images_by_mode`.
17
+ * The three pre-rename names remain importable as soft-deprecated aliases through 2.0.0 (https://github.com/clamsproject/mmif-python/pull/385).
18
+ * `mmif.utils.workflow_helper.generate_workflow_identifier`: now prefixes a `source_composition` segment (`TextDocument-1-VideoDocument-1/...`) describing the top-level document mix in the input MMIF (https://github.com/clamsproject/mmif-python/pull/386).
19
+ * `build-tools/docs.py`: versioned-docs build now works against pyproject-only tags, restoring docs publishing in the release workflow (https://github.com/clamsproject/mmif-python/pull/388, https://github.com/clamsproject/mmif-python/issues/387).
20
+
21
+ > [!NOTE]
22
+ > - `mmif describe` (and the underlying `mmif.utils.workflow_helper`) is still experimental and subject to change in future releases without notice. Backward compatibility is not guaranteed.
23
+ > - The new `source_composition` prefix in `generate_workflow_identifier()` changes the format of returned identifiers. Downstream tools parsing the identifier path (e.g. for storage-directory routing) may need to account for the extra leading segment.
24
+
25
+
26
+
2
27
  ## releasing 1.4.0 (2026-04-22)
3
28
  ### Overview
4
29
  * This is minor level release that 1) replacing simple string enums in `mmif.vocabulary` with fully-fledged pydantic classes in `clams-vocabulary` package 2) deprecating framenum-based image extraction functions in VDH (frame numbers are no longer considered as proper timestamps)
@@ -13,10 +13,17 @@ We follow a Gitflow-inspired branching model to maintain a stable `main` branch
13
13
  - Once work is complete, open a PR targeting the `develop` branch.
14
14
  - **Communication**: High-level discussion and planning should occur in the issue thread. The PR conversation is strictly for code review and implementation-specific feedback.
15
15
  5. **Releases**:
16
- - When `develop` is ready for a new release, open a PR from `develop` to `main` using the "release" PR template.
16
+ - When `develop` is ready for a new release, open a PR from `develop` to `main` using the "release" PR template. See the **Releases** section below for the required title format and preparation steps.
17
17
  - After merging the release candidate into `main`, manually tag the commit with the version number. This tag triggers the automated CI/CD pipeline for publishing.
18
18
  6. **Branch Protection**: Both `main` and `develop` are protected branches. Direct pushes are disabled; all changes must be introduced via Pull Requests.
19
19
 
20
+ ## Releases
21
+
22
+ Release PRs (from `develop` to `main`) are gated by the `release-check` CI workflow. Before opening one:
23
+
24
+ 1. Title the PR exactly `releasing X.Y.Z` (strict semver); `release-check` reads the version from the title.
25
+ 2. Run `python build-tools/prep_release.py X.Y.Z` and commit its changes. See inside the prep script to see what's actually prepared.
26
+
20
27
  ## CLI Scripts
21
28
 
22
29
  The `mmif` command-line interface supports subcommands (e.g., `mmif source`, `mmif describe`). These are implemented as Python modules in `mmif/utils/cli/`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmif-python
3
- Version: 1.5.0
3
+ Version: 1.5.1
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
@@ -0,0 +1,91 @@
1
+ """
2
+ Prepare release artifacts for the mmif-python package.
3
+
4
+ Run while preparing a release PR, passing the version being released::
5
+
6
+ python build-tools/prep_release.py X.Y.Z
7
+
8
+ Currently this adds a row to ``documentation/target-versions.csv`` for
9
+ the release version, resolving the target MMIF spec version from the
10
+ installed package metadata (``mmif.__specver__``). This script is the
11
+ single place that writes the target-versions table; the docs build only
12
+ renders it, and CI verifies (on the release PR) that a row for the PR's
13
+ version exists.
14
+
15
+ Requires an editable install (``pip install -e .``) so that
16
+ ``mmif.__specver__`` is importable.
17
+ """
18
+ import argparse
19
+ import re
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ PROJECT_ROOT = Path(__file__).parent.parent
24
+ CSV_PATH = PROJECT_ROOT / 'documentation' / 'target-versions.csv'
25
+
26
+ VERSION_RE = re.compile(r'^\d+\.\d+\.\d+$')
27
+ TOP_ROW_RE = re.compile(r'`(\d+\.\d+\.\d+) <')
28
+
29
+
30
+ def _row(version, specver):
31
+ return (
32
+ f'`{version} <https://pypi.org/project/mmif-python/{version}/>`__,'
33
+ f'`{specver} <https://mmif.clams.ai/{specver}/>`__'
34
+ )
35
+
36
+
37
+ def _version_tuple(v):
38
+ return tuple(int(x) for x in v.split('.'))
39
+
40
+
41
+ def _top_version(data_lines):
42
+ if not data_lines:
43
+ return None
44
+ m = TOP_ROW_RE.match(data_lines[0])
45
+ return m.group(1) if m else None
46
+
47
+
48
+ def main():
49
+ parser = argparse.ArgumentParser(
50
+ description="Prepare release artifacts for mmif-python."
51
+ )
52
+ parser.add_argument(
53
+ "version", metavar="X.Y.Z",
54
+ help="the release version being prepared",
55
+ )
56
+ args = parser.parse_args()
57
+ version = args.version
58
+
59
+ if not VERSION_RE.match(version):
60
+ sys.exit(f"Error: '{version}' is not a valid X.Y.Z version.")
61
+
62
+ try:
63
+ import mmif
64
+ specver = mmif.__specver__
65
+ except (ImportError, AttributeError):
66
+ sys.exit("Error: cannot read mmif.__specver__. "
67
+ "Run `pip install -e .` first.")
68
+
69
+ lines = CSV_PATH.read_text().splitlines()
70
+ header, data = lines[0], lines[1:]
71
+ top = _top_version(data)
72
+
73
+ if top is not None and _version_tuple(version) < _version_tuple(top):
74
+ sys.exit(f"Error: {version} is older than the current top row "
75
+ f"{top}; refusing to insert an out-of-order version.")
76
+
77
+ new_row = _row(version, specver)
78
+ if top == version:
79
+ data[0] = new_row # update in place (e.g. spec changed)
80
+ action = "updated"
81
+ else:
82
+ data.insert(0, new_row) # prepend as the new latest
83
+ action = "added"
84
+
85
+ CSV_PATH.write_text('\n'.join([header] + data) + '\n')
86
+ print(f"target-versions.csv: {action} row "
87
+ f"{version} -> MMIF spec {specver}")
88
+
89
+
90
+ if __name__ == "__main__":
91
+ main()
@@ -6,7 +6,6 @@ import datetime
6
6
  import inspect
7
7
  import json
8
8
  import os
9
- import re
10
9
  import subprocess
11
10
  import sys
12
11
  import textwrap
@@ -148,42 +147,6 @@ def linkcode_resolve(domain, info):
148
147
  return None
149
148
 
150
149
 
151
- def update_target_versions(app):
152
- """
153
- Update documentation/target-versions.csv with the current version and spec version.
154
- This replaces the old logic in setup.py.
155
- """
156
- # Check if we have __specver__ (it might not be there if not installed via setup.py develop)
157
- if not hasattr(mmif, '__specver__'):
158
- return
159
-
160
- current_ver = mmif.__version__
161
- # Skip dev/dummy versions to avoid dirtying the git-tracked CSV
162
- if 'dev' in current_ver or not re.match(r'^\d+\.\d+\.\d+$', current_ver):
163
- return
164
- spec_ver = mmif.__specver__
165
-
166
- csv_path = proj_root_dir / 'documentation' / 'target-versions.csv'
167
- if not csv_path.exists():
168
- return
169
-
170
- # Read existing content
171
- with open(csv_path, 'r') as f:
172
- lines = f.readlines()
173
-
174
- # Check if current version is already in the file (first data line)
175
- # lines[0] is header, lines[1] is latest version
176
- if len(lines) > 1 and lines[1].startswith(f'{current_ver},'):
177
- return
178
-
179
- # Insert new version
180
- logger.info(f"Updating target-versions.csv: {current_ver} -> {spec_ver}")
181
- lines.insert(1, f'{current_ver},"{spec_ver}"\n')
182
-
183
- with open(csv_path, 'w') as f:
184
- f.writelines(lines)
185
-
186
-
187
150
  def generate_cli_rst(app):
188
151
  from mmif import prep_argparser_and_subcmds
189
152
 
@@ -292,7 +255,6 @@ def run_apidoc(app):
292
255
  def setup(app):
293
256
  try:
294
257
  app.connect('builder-inited', run_apidoc)
295
- app.connect('builder-inited', update_target_versions)
296
258
  app.connect('builder-inited', generate_cli_rst)
297
259
  app.connect('builder-inited', generate_whatsnew_rst)
298
260
  except ImportError:
@@ -0,0 +1,52 @@
1
+ "``mmif-python`` version","Target MMIF Specification"
2
+ `1.5.1 <https://pypi.org/project/mmif-python/1.5.1/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
3
+ `1.5.0 <https://pypi.org/project/mmif-python/1.5.0/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
4
+ `1.4.0 <https://pypi.org/project/mmif-python/1.4.0/>`__,`1.1.1 <https://mmif.clams.ai/1.1.1/>`__
5
+ `1.3.1 <https://pypi.org/project/mmif-python/1.3.1/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
6
+ `1.3.0 <https://pypi.org/project/mmif-python/1.3.0/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
7
+ `1.2.1 <https://pypi.org/project/mmif-python/1.2.1/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
8
+ `1.2.0 <https://pypi.org/project/mmif-python/1.2.0/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
9
+ `1.1.2 <https://pypi.org/project/mmif-python/1.1.2/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
10
+ `1.1.1 <https://pypi.org/project/mmif-python/1.1.1/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
11
+ `1.1.0 <https://pypi.org/project/mmif-python/1.1.0/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
12
+ `1.0.19 <https://pypi.org/project/mmif-python/1.0.19/>`__,`1.0.5 <https://mmif.clams.ai/1.0.5/>`__
13
+ `1.0.18 <https://pypi.org/project/mmif-python/1.0.18/>`__,`1.0.5 <https://mmif.clams.ai/1.0.5/>`__
14
+ `1.0.17 <https://pypi.org/project/mmif-python/1.0.17/>`__,`1.0.5 <https://mmif.clams.ai/1.0.5/>`__
15
+ `1.0.16 <https://pypi.org/project/mmif-python/1.0.16/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
16
+ `1.0.15 <https://pypi.org/project/mmif-python/1.0.15/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
17
+ `1.0.14 <https://pypi.org/project/mmif-python/1.0.14/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
18
+ `1.0.13 <https://pypi.org/project/mmif-python/1.0.13/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
19
+ `1.0.12 <https://pypi.org/project/mmif-python/1.0.12/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
20
+ `1.0.11 <https://pypi.org/project/mmif-python/1.0.11/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
21
+ `1.0.10 <https://pypi.org/project/mmif-python/1.0.10/>`__,`1.0.2 <https://mmif.clams.ai/1.0.2/>`__
22
+ `1.0.9 <https://pypi.org/project/mmif-python/1.0.9/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
23
+ `1.0.8 <https://pypi.org/project/mmif-python/1.0.8/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
24
+ `1.0.7 <https://pypi.org/project/mmif-python/1.0.7/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
25
+ `1.0.6 <https://pypi.org/project/mmif-python/1.0.6/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
26
+ `1.0.5 <https://pypi.org/project/mmif-python/1.0.5/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
27
+ `1.0.4 <https://pypi.org/project/mmif-python/1.0.4/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
28
+ `1.0.3 <https://pypi.org/project/mmif-python/1.0.3/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
29
+ `1.0.2 <https://pypi.org/project/mmif-python/1.0.2/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
30
+ `1.0.1 <https://pypi.org/project/mmif-python/1.0.1/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
31
+ `1.0.0 <https://pypi.org/project/mmif-python/1.0.0/>`__,`0.5.0 <https://mmif.clams.ai/0.5.0/>`__
32
+ `0.5.2 <https://pypi.org/project/mmif-python/0.5.2/>`__,`0.5.0 <https://mmif.clams.ai/0.5.0/>`__
33
+ `0.5.1 <https://pypi.org/project/mmif-python/0.5.1/>`__,`0.5.0 <https://mmif.clams.ai/0.5.0/>`__
34
+ `0.5.0 <https://pypi.org/project/mmif-python/0.5.0/>`__,`0.5.0 <https://mmif.clams.ai/0.5.0/>`__
35
+ `0.4.8 <https://pypi.org/project/mmif-python/0.4.8/>`__,`0.4.2 <https://mmif.clams.ai/0.4.2/>`__
36
+ `0.4.7 <https://pypi.org/project/mmif-python/0.4.7/>`__,`0.4.1 <https://mmif.clams.ai/0.4.1/>`__
37
+ `0.4.6 <https://pypi.org/project/mmif-python/0.4.6/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
38
+ `0.4.5 <https://pypi.org/project/mmif-python/0.4.5/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
39
+ `0.4.4 <https://pypi.org/project/mmif-python/0.4.4/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
40
+ `0.4.3 <https://pypi.org/project/mmif-python/0.4.3/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
41
+ `0.4.2 <https://pypi.org/project/mmif-python/0.4.2/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
42
+ `0.4.1 <https://pypi.org/project/mmif-python/0.4.1/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
43
+ `0.4.0 <https://pypi.org/project/mmif-python/0.4.0/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
44
+ `0.3.5 <https://pypi.org/project/mmif-python/0.3.5/>`__,`0.3.1 <https://mmif.clams.ai/0.3.1/>`__
45
+ `0.3.4 <https://pypi.org/project/mmif-python/0.3.4/>`__,`0.3.1 <https://mmif.clams.ai/0.3.1/>`__
46
+ `0.3.3 <https://pypi.org/project/mmif-python/0.3.3/>`__,`0.3.1 <https://mmif.clams.ai/0.3.1/>`__
47
+ `0.3.2 <https://pypi.org/project/mmif-python/0.3.2/>`__,`0.3.1 <https://mmif.clams.ai/0.3.1/>`__
48
+ `0.3.1 <https://pypi.org/project/mmif-python/0.3.1/>`__,`0.3.0 <https://mmif.clams.ai/0.3.0/>`__
49
+ `0.3.0 <https://pypi.org/project/mmif-python/0.3.0/>`__,`0.3.0 <https://mmif.clams.ai/0.3.0/>`__
50
+ `0.2.2 <https://pypi.org/project/mmif-python/0.2.2/>`__,`0.2.1 <https://mmif.clams.ai/0.2.1/>`__
51
+ `0.2.1 <https://pypi.org/project/mmif-python/0.2.1/>`__,`0.2.1 <https://mmif.clams.ai/0.2.1/>`__
52
+ `0.2.0 <https://pypi.org/project/mmif-python/0.2.0/>`__,`0.2.1 <https://mmif.clams.ai/0.2.1/>`__
@@ -414,22 +414,56 @@ class Document(Annotation):
414
414
  else:
415
415
  super().add_property(name, value)
416
416
 
417
- def get(self, prop_name, default=None):
417
+ def __getitem__(self, prop_name: str):
418
418
  """
419
- Safe property access with optional default value for Document objects.
419
+ Property access for ``Document`` objects, following the document
420
+ property hierarchy (highest to lowest priority):
420
421
 
421
- Searches for a document property by name and returns its value, or a
422
- default value if not found. Documents have a more complex property
423
- hierarchy than regular annotations:
422
+ #. Special fields (``id``, ``location``)
423
+ #. Pending properties (added via :meth:`add_property` after creation,
424
+ to be serialized as capital ``Annotation`` objects)
425
+ #. Original properties (in ``document.properties``)
426
+ #. Ephemeral properties (from existing ``Annotation`` objects or view
427
+ ``contains`` metadata)
424
428
 
425
- Priority order (highest to lowest):
426
- 1. Special fields ('id', 'location')
427
- 2. Pending properties (added after creation, to be serialized as ``Annotation`` objects)
428
- 3. Ephemeral properties (from existing ``Annotation`` annotations or view metadata)
429
- 4. Original properties (in ``document.properties``)
429
+ ``Document`` overrides ``__getitem__`` (not only :meth:`get`) so that
430
+ the ``in`` operator, subscripting, and :meth:`get` all agree.
431
+ ``__contains__`` is implemented on top of ``__getitem__``, so without
432
+ this override a property added via :meth:`add_property` (which lands in
433
+ ``_props_pending``) is readable through :meth:`get` yet invisible to
434
+ ``in`` until a serialize/deserialize round trip. See issue #390.
430
435
 
431
- This allows convenient access to all document properties regardless of
432
- where they're stored internally.
436
+ :param prop_name: the name of the property to retrieve
437
+ :raises KeyError: if the property is not found in any of the stores
438
+ """
439
+ if prop_name == 'id':
440
+ # because all three dicts have `id` key as required field, we need
441
+ # this special case to return the correct value from the correct dict
442
+ return self.id
443
+ elif prop_name == 'location':
444
+ # location is internally stored as `location_`, so the regular
445
+ # lookup below never finds it. Treat an unset location as absent
446
+ # (raise) rather than returning None, so the inherited
447
+ # `__contains__` does not report `'location'` as always present.
448
+ if self.location is None:
449
+ raise KeyError(prop_name)
450
+ return self.location
451
+ elif prop_name in self._props_pending:
452
+ return self._props_pending[prop_name]
453
+ else:
454
+ # Delegates to Annotation.__getitem__(), which checks
455
+ # self.properties (_props_original) before _props_ephemeral.
456
+ # This gives annotation-level properties precedence over
457
+ # view-level contains defaults (MMIF spec >= 1.1.1).
458
+ return super().__getitem__(prop_name)
459
+
460
+ def get(self, prop_name, default=None):
461
+ """
462
+ Safe property access with optional default value for Document objects.
463
+
464
+ Searches for a document property by name and returns its value, or
465
+ ``default`` if not found, following the priority order documented on
466
+ :meth:`__getitem__`.
433
467
 
434
468
  :param prop_name: The name of the property to retrieve
435
469
  :param default: The value to return if the property is not found (default: None)
@@ -455,22 +489,10 @@ class Document(Annotation):
455
489
  add_property : Add a new property to the document
456
490
  mmif.serialize.mmif.Mmif.generate_capital_annotations : How pending properties are serialized
457
491
  """
458
- if prop_name == 'id':
459
- # because all three dicts have `id` key as required field, we need
460
- # this special case to return the correct value from the correct dict
461
- return self.id
462
- elif prop_name == 'location':
463
- # because location is internally stored in self.location_,
464
- # it doesn't work with regular __getitem__ method
465
- return self.location
466
- elif prop_name in self._props_pending:
467
- return self._props_pending[prop_name]
468
- else:
469
- # Delegates to Annotation.__getitem__(), which checks
470
- # self.properties (_props_original) before _props_ephemeral.
471
- # This gives annotation-level properties precedence over
472
- # view-level contains defaults (MMIF spec >= 1.1.1).
473
- return super().get(prop_name, default)
492
+ try:
493
+ return self.__getitem__(prop_name)
494
+ except KeyError:
495
+ return default
474
496
 
475
497
  get_property = get
476
498
 
@@ -96,20 +96,36 @@ def open_container(video_document: Document):
96
96
  container = av.open(video_document.location_path(nonexist_ok=False))
97
97
  stream = container.streams.video[0]
98
98
  time_base = float(stream.time_base)
99
- fps = round(float(stream.average_rate), 2)
99
+ # Frame rate: prefer ffmpeg's guessed rate (av_guess_frame_rate), which
100
+ # falls back to the stream's base rate (r_frame_rate) when the average
101
+ # rate (avg_frame_rate) is unset. Matroska/webm commonly leave
102
+ # `average_rate` as None (which would crash float()) or 0, so it cannot be
103
+ # used directly. `rate` stays a Fraction for exact frame-count math; both
104
+ # None and a zero rate are falsy, so the chain yields the first usable one.
105
+ rate = stream.guessed_rate or stream.average_rate or stream.base_rate
106
+ fps = round(float(rate), 2) if rate else 0.0
107
+ # Resolve duration (in seconds) from the most reliable source available.
108
+ # `stream.duration` is exact for CFR H.264/MP4, but Matroska/webm leave it
109
+ # unset and carry the length only at the container level
110
+ # (`container.duration`, in `av.time_base` microseconds).
111
+ if stream.duration is not None:
112
+ duration_s = float(stream.duration) * time_base
113
+ elif container.duration is not None:
114
+ duration_s = container.duration / av.time_base
115
+ else:
116
+ duration_s = None
100
117
  # `stream.frames` comes from the container header. Verified exact on
101
118
  # CFR H.264/MP4 inputs even with non-zero start offset; for VFR or
102
119
  # headerless streams it may be 0, in which case `duration * rate` is
103
120
  # the best available (approximate) estimate.
104
121
  if stream.frames > 0:
105
122
  frame_count = stream.frames
106
- elif stream.duration is not None and stream.average_rate is not None:
107
- frame_count = int(round(float(stream.duration) * time_base
108
- * float(stream.average_rate)))
123
+ elif duration_s is not None and rate:
124
+ frame_count = int(round(duration_s * float(rate)))
109
125
  else:
110
126
  frame_count = 0
111
- if stream.duration is not None:
112
- duration_ms = int(round(float(stream.duration) * time_base * 1000))
127
+ if duration_s is not None:
128
+ duration_ms = int(round(duration_s * 1000))
113
129
  elif frame_count > 0 and fps > 0:
114
130
  duration_ms = int(round(frame_count / fps * 1000))
115
131
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmif-python
3
- Version: 1.5.0
3
+ Version: 1.5.1
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
@@ -9,9 +9,11 @@ pyproject.toml
9
9
  .github/workflows/issue-close.yml
10
10
  .github/workflows/issue-infra-project.yml
11
11
  .github/workflows/publish.yml
12
+ .github/workflows/release-check.yml
12
13
  build-tools/build.py
13
14
  build-tools/clean.py
14
15
  build-tools/docs.py
16
+ build-tools/prep_release.py
15
17
  build-tools/publish.py
16
18
  build-tools/test.py
17
19
  documentation/cli.rst
@@ -63,6 +65,7 @@ mmif_python.egg-info/entry_points.txt
63
65
  mmif_python.egg-info/requires.txt
64
66
  mmif_python.egg-info/top_level.txt
65
67
  tests/black-2997fps.mp4
68
+ tests/black-2997fps.webm
66
69
  tests/mmif_examples.py
67
70
  tests/test_serialize.py
68
71
  tests/test_specver.py
@@ -1304,7 +1304,36 @@ class TestDocument(unittest.TestCase):
1304
1304
  doc1_mmif_roundtrip = mmif_roundtrip['doc1']
1305
1305
  self.assertEqual(0, len(doc1_mmif_roundtrip._props_pending))
1306
1306
  self.assertEqual('me', doc1_mmif_roundtrip.get_property('author'))
1307
-
1307
+
1308
+ def test_document_pending_property_membership(self):
1309
+ # regression for issue #390: a property added via `add_property`
1310
+ # lands in `_props_pending` and must be visible through `get_property`,
1311
+ # subscripting, AND the `in` operator consistently -- without needing a
1312
+ # serialize/deserialize round trip first.
1313
+ doc = Document()
1314
+ doc.at_type = DocumentTypes.VideoDocument
1315
+ doc.id = 'doc1'
1316
+ doc.add_property('fps', 29.97)
1317
+ self.assertIn('fps', doc._props_pending)
1318
+ # all three access paths must agree
1319
+ self.assertEqual(29.97, doc.get_property('fps'))
1320
+ self.assertEqual(29.97, doc['fps'])
1321
+ self.assertIn('fps', doc)
1322
+ # special fields and absent keys are unaffected
1323
+ self.assertIn('id', doc)
1324
+ self.assertEqual('doc1', doc['id'])
1325
+ self.assertNotIn('nope', doc)
1326
+ with self.assertRaises(KeyError):
1327
+ _ = doc['nope']
1328
+ # an unset `location` must read as absent, not as always-present None
1329
+ self.assertNotIn('location', doc)
1330
+ with self.assertRaises(KeyError):
1331
+ _ = doc['location']
1332
+ self.assertIsNone(doc.get('location'))
1333
+ doc.location = 'file:///tmp/doc1.mp4'
1334
+ self.assertIn('location', doc)
1335
+ self.assertEqual('file:///tmp/doc1.mp4', doc['location'])
1336
+
1308
1337
  def test_document_adding_duplicate_properties(self):
1309
1338
  mmif = Mmif(self.data['everything']['string'])
1310
1339
  doc1 = Document()
@@ -183,6 +183,26 @@ class TestVideoDocumentHelper(unittest.TestCase):
183
183
  finally:
184
184
  c.close()
185
185
 
186
+ def test_open_container_matroska(self):
187
+ # regression for issue #390: Matroska/webm leave the per-stream
188
+ # `frames`/`duration` fields unset, so open_container must fall back to
189
+ # the container-level duration instead of caching frameCount/duration 0.
190
+ vd = Document({
191
+ "@type": "http://mmif.clams.ai/vocabulary/VideoDocument/v1",
192
+ "properties": {
193
+ "mime": "video",
194
+ "id": "o1",
195
+ "location": f"file://{pathlib.Path(__file__).parent}/black-2997fps.webm",
196
+ }
197
+ })
198
+ c = vdh.open_container(vd)
199
+ try:
200
+ self.assertAlmostEqual(29.97, vd.get_property('fps'), places=1)
201
+ self.assertGreater(vd.get_property('frameCount'), 0)
202
+ self.assertGreater(vd.get_property('duration'), 0)
203
+ finally:
204
+ c.close()
205
+
186
206
  def test_sample_timepoints(self):
187
207
  # half-open interval; step in ms
188
208
  self.assertEqual([0, 100, 200, 300, 400],
@@ -1,47 +0,0 @@
1
- "``mmif-python`` version","Target MMIF Specification"
2
- 1.3.0,"1.1.0"
3
- 1.2.0,"1.1.0"
4
- 1.1.2,"1.1.0"
5
- 1.1.1,"1.1.0"
6
- 1.1.0,"1.1.0"
7
- 1.0.19,"1.0.5"
8
- 1.0.18,"1.0.5"
9
- 1.0.17,"1.0.5"
10
- 1.0.16,"1.0.4"
11
- 1.0.15,"1.0.4"
12
- 1.0.14,"1.0.4"
13
- 1.0.13,"1.0.4"
14
- 1.0.12,"1.0.4"
15
- 1.0.11,"1.0.4"
16
- 1.0.10,"1.0.2"
17
- 1.0.9,"1.1.0"
18
- 1.0.8,"1.0.0"
19
- 1.0.7,"1.0.0"
20
- 1.0.6,"1.0.0"
21
- 1.0.5,"1.0.0"
22
- 1.0.4,"1.0.0"
23
- 1.0.3,"1.0.0"
24
- 1.0.2,"1.0.0"
25
- 1.0.1,"1.0.0"
26
- 1.0.0,"0.5.0"
27
- 0.5.2,"0.5.0"
28
- 0.5.1,"0.5.0"
29
- 0.5.0,"0.5.0"
30
- 0.4.8,"0.4.2"
31
- 0.4.7,"0.4.1"
32
- 0.4.6,"0.4.0"
33
- 0.4.5,"0.4.0"
34
- 0.4.4,"0.4.0"
35
- 0.4.3,"0.4.0"
36
- 0.4.2,"0.4.0"
37
- 0.4.1,"0.4.0"
38
- 0.4.0,"0.4.0"
39
- 0.3.5,"0.3.1"
40
- 0.3.4,"0.3.1"
41
- 0.3.3,"0.3.1"
42
- 0.3.2,"0.3.1"
43
- 0.3.1,"0.3.0"
44
- 0.3.0,"0.3.0"
45
- 0.2.2,"0.2.1"
46
- 0.2.1,"0.2.1"
47
- 0.2.0,"0.2.1"
File without changes
File without changes
File without changes
File without changes
File without changes