mmif-python 1.5.0__tar.gz → 1.5.2__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 (102) hide show
  1. mmif_python-1.5.2/.github/workflows/release-check.yml +11 -0
  2. {mmif_python-1.5.0 → mmif_python-1.5.2}/CHANGELOG.md +30 -0
  3. {mmif_python-1.5.0 → mmif_python-1.5.2}/CONTRIBUTING.md +9 -2
  4. {mmif_python-1.5.0/mmif_python.egg-info → mmif_python-1.5.2}/PKG-INFO +1 -1
  5. {mmif_python-1.5.0 → mmif_python-1.5.2}/build-tools/docs.py +23 -8
  6. mmif_python-1.5.2/build-tools/prep_release.py +91 -0
  7. {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/conf.py +0 -38
  8. mmif_python-1.5.2/documentation/target-versions.csv +53 -0
  9. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/serialize/annotation.py +50 -28
  10. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/serialize/view.py +19 -0
  11. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/video_document_helper.py +22 -6
  12. {mmif_python-1.5.0 → mmif_python-1.5.2/mmif_python.egg-info}/PKG-INFO +1 -1
  13. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif_python.egg-info/SOURCES.txt +5 -0
  14. mmif_python-1.5.2/mmif_python.egg-info/scm_file_list.json +95 -0
  15. mmif_python-1.5.2/mmif_python.egg-info/scm_version.json +8 -0
  16. mmif_python-1.5.2/tests/black-2997fps.webm +0 -0
  17. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/test_serialize.py +81 -2
  18. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/test_utils.py +20 -0
  19. mmif_python-1.5.0/documentation/target-versions.csv +0 -47
  20. {mmif_python-1.5.0 → mmif_python-1.5.2}/.github/workflows/codecov.yml +0 -0
  21. {mmif_python-1.5.0 → mmif_python-1.5.2}/.github/workflows/issue-assign.yml +0 -0
  22. {mmif_python-1.5.0 → mmif_python-1.5.2}/.github/workflows/issue-close.yml +0 -0
  23. {mmif_python-1.5.0 → mmif_python-1.5.2}/.github/workflows/issue-infra-project.yml +0 -0
  24. {mmif_python-1.5.0 → mmif_python-1.5.2}/.github/workflows/publish.yml +0 -0
  25. {mmif_python-1.5.0 → mmif_python-1.5.2}/.gitignore +0 -0
  26. {mmif_python-1.5.0 → mmif_python-1.5.2}/LICENSE +0 -0
  27. {mmif_python-1.5.0 → mmif_python-1.5.2}/README.md +0 -0
  28. {mmif_python-1.5.0 → mmif_python-1.5.2}/build-tools/build.py +0 -0
  29. {mmif_python-1.5.0 → mmif_python-1.5.2}/build-tools/clean.py +0 -0
  30. {mmif_python-1.5.0 → mmif_python-1.5.2}/build-tools/publish.py +0 -0
  31. {mmif_python-1.5.0 → mmif_python-1.5.2}/build-tools/test.py +0 -0
  32. {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/_mmif_example_builder/__init__.py +0 -0
  33. {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/_templates/versions.html +0 -0
  34. {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/cli.rst +0 -0
  35. {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/consumer-tutorial.rst +0 -0
  36. {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/index.rst +0 -0
  37. {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/introduction.rst +0 -0
  38. {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/plugins.rst +0 -0
  39. {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/summarizer.rst +0 -0
  40. {mmif_python-1.5.0 → mmif_python-1.5.2}/documentation/target-versions.rst +0 -0
  41. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/__init__.py +0 -0
  42. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/res/__init__.py +0 -0
  43. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/res/mmif.json +0 -0
  44. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/serialize/__init__.py +0 -0
  45. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/serialize/mmif.py +0 -0
  46. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/serialize/model.py +0 -0
  47. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/__init__.py +0 -0
  48. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/cli/__init__.py +0 -0
  49. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/cli/describe.py +0 -0
  50. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/cli/rewind.py +0 -0
  51. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/cli/source.py +0 -0
  52. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/cli/summarize.py +0 -0
  53. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/sequence_helper.py +0 -0
  54. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/summarizer/__init__.py +0 -0
  55. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/summarizer/config.py +0 -0
  56. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/summarizer/graph.py +0 -0
  57. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/summarizer/nodes.py +0 -0
  58. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/summarizer/summary.py +0 -0
  59. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/summarizer/utils.py +0 -0
  60. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/text_document_helper.py +0 -0
  61. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/timeunit_helper.py +0 -0
  62. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/utils/workflow_helper.py +0 -0
  63. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/ver/__init__.py +0 -0
  64. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/vocabulary/__init__.py +0 -0
  65. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/vocabulary/annotation_types.py +0 -0
  66. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/vocabulary/base_types.py +0 -0
  67. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif/vocabulary/document_types.py +0 -0
  68. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif_docloc_http/__init__.py +0 -0
  69. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif_python.egg-info/dependency_links.txt +0 -0
  70. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif_python.egg-info/entry_points.txt +0 -0
  71. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif_python.egg-info/requires.txt +0 -0
  72. {mmif_python-1.5.0 → mmif_python-1.5.2}/mmif_python.egg-info/top_level.txt +0 -0
  73. {mmif_python-1.5.0 → mmif_python-1.5.2}/pyproject.toml +0 -0
  74. {mmif_python-1.5.0 → mmif_python-1.5.2}/setup.cfg +0 -0
  75. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/black-2997fps.mp4 +0 -0
  76. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/1.0.5-old-shortid.json +0 -0
  77. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/README.md +0 -0
  78. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/bars-tones-slates/raw.json +0 -0
  79. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/east-tesseract-typing/raw.json +0 -0
  80. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/everything/raw.json +0 -0
  81. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/examples.json +0 -0
  82. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/README.md +0 -0
  83. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/image-box-ocr-timex.json +0 -0
  84. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/image-box-ocr.json +0 -0
  85. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/image-box.json +0 -0
  86. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/image.json +0 -0
  87. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/slate-detect.json +0 -0
  88. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/slate-reco.json +0 -0
  89. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/video-demux.json +0 -0
  90. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/video-transcript-demux-fa.json +0 -0
  91. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/video-transcript-demux-fa.short.json +0 -0
  92. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/video-transcript-demux.json +0 -0
  93. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/video-transcript-sentencesplit.json +0 -0
  94. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/video-transcript.json +0 -0
  95. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/others/video.json +0 -0
  96. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/segmenter-kaldi-ner/raw.json +0 -0
  97. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif-examples/swt-1.0.mmif +0 -0
  98. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/mmif_examples.py +0 -0
  99. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/test_specver.py +0 -0
  100. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/test_utils_cli.py +0 -0
  101. {mmif_python-1.5.0 → mmif_python-1.5.2}/tests/test_vocab.py +0 -0
  102. {mmif_python-1.5.0 → mmif_python-1.5.2}/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,34 @@
1
1
 
2
+ ## releasing 1.5.1 (2026-06-19)
3
+ ### Overview
4
+ 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.
5
+
6
+
7
+ ## releasing 1.5.0 (2026-05-27)
8
+ ### Overview
9
+
10
+ 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.
11
+
12
+ ### Additions
13
+
14
+ * `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).
15
+
16
+ ### Changes
17
+
18
+ * `mmif.utils.video_document_helper`: renamed
19
+ * `extract_timepoints_as_images` --> `extract_images_from_timepoints`,
20
+ * `extract_target_frames` --> `extract_images_by_count_with_sources` (alongside new bare `extract_images_by_count`), and
21
+ * `extract_frames_by_mode` --> `extract_images_by_mode`.
22
+ * The three pre-rename names remain importable as soft-deprecated aliases through 2.0.0 (https://github.com/clamsproject/mmif-python/pull/385).
23
+ * `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).
24
+ * `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).
25
+
26
+ > [!NOTE]
27
+ > - `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.
28
+ > - 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.
29
+
30
+
31
+
2
32
  ## releasing 1.4.0 (2026-04-22)
3
33
  ### Overview
4
34
  * 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/`.
@@ -155,7 +162,7 @@ To build documentation for a specific historical version (e.g., `v1.0.0`):
155
162
  python3 build-tools/docs.py --build-ver v1.0.0
156
163
  ```
157
164
 
158
- This runs the build in a sandboxed temporary directory. The output will be in `docs-test/<version>`.
165
+ This runs the build in a sandboxed temporary directory. A single `--build-ver` places the built docs **flat** in the output directory (`docs-test/` by default). The batch `--build-all-since` mode instead nests each version under a `<version>/` subdirectory of the output directory, so multiple versions can coexist. The flat single-version layout is what the CI docs-publish workflow relies on — it invokes `docs.py --build-ver <ver> --output-dir _docs` and uploads `_docs/` as-is, without needing to know whether the project is versioned.
159
166
 
160
167
  > [!NOTE]
161
168
  > In CI, documentation is built and published automatically by the `publish.yml` workflow via the shared `sdk-docs.yml`. The CI calls `docs.py --build-ver <version> --output-dir _docs`. All CLAMS SDK repos use the same `docs.py` CLI interface (`--build-ver`, `--output-dir`).
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmif-python
3
- Version: 1.5.0
3
+ Version: 1.5.2
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
@@ -188,7 +188,8 @@ def linkcode_resolve(domain, info):
188
188
  return docs_build_dir
189
189
 
190
190
 
191
- def build_docs_for_version(version: str, output_base_dir: Path, repo_path: Path = None):
191
+ def build_docs_for_version(version: str, output_base_dir: Path, repo_path: Path = None,
192
+ versioned_subdir: bool = True):
192
193
  """
193
194
  Builds docs for a specific version in a sandbox.
194
195
  Uses local repo copy instead of cloning for efficiency.
@@ -197,6 +198,12 @@ def build_docs_for_version(version: str, output_base_dir: Path, repo_path: Path
197
198
  version: Git tag/ref to build
198
199
  output_base_dir: Directory to output built docs
199
200
  repo_path: Path to local repo to copy from (defaults to cwd)
201
+ versioned_subdir: when True (batch/from-scratch rebuild), the built
202
+ docs are placed under ``output_base_dir/<version>/`` so multiple
203
+ versions can coexist. When False (single build, as used by CI),
204
+ the docs are placed flat into ``output_base_dir`` so the shared
205
+ docs-publish workflow can upload them as-is without knowing about
206
+ versioning.
200
207
  """
201
208
  print(f"--- Running in Version Build Mode for version: {version} ---")
202
209
 
@@ -226,13 +233,19 @@ def build_docs_for_version(version: str, output_base_dir: Path, repo_path: Path
226
233
  built_docs_path = build_versioned_docs(env, source_path, version)
227
234
 
228
235
  print(f"\n--- Step 4: Copying built artifacts to output directory ---")
229
- version_output_dir = output_base_dir / version
230
- if version_output_dir.exists():
231
- print(f"Removing existing directory: {version_output_dir}")
232
- shutil.rmtree(version_output_dir)
233
- shutil.copytree(built_docs_path, version_output_dir)
236
+ if versioned_subdir:
237
+ dest = output_base_dir / version
238
+ if dest.exists():
239
+ print(f"Removing existing directory: {dest}")
240
+ shutil.rmtree(dest)
241
+ shutil.copytree(built_docs_path, dest)
242
+ else:
243
+ # flat: place the built docs directly into output_base_dir
244
+ output_base_dir.mkdir(parents=True, exist_ok=True)
245
+ shutil.copytree(built_docs_path, output_base_dir, dirs_exist_ok=True)
246
+ dest = output_base_dir
234
247
 
235
- print(f"\nDocumentation for {version} built successfully in: {version_output_dir}")
248
+ print(f"\nDocumentation for {version} built successfully in: {dest}")
236
249
 
237
250
 
238
251
  def get_all_tags():
@@ -317,7 +330,9 @@ def main():
317
330
  for version, error in failed:
318
331
  print(f" {version}: {error}")
319
332
  elif args.build_ver:
320
- build_docs_for_version(args.build_ver, output_dir)
333
+ # single build (CI + local single-version): flat output so the
334
+ # shared docs-publish workflow uploads it version-agnostically
335
+ build_docs_for_version(args.build_ver, output_dir, versioned_subdir=False)
321
336
  else:
322
337
  build_docs_local(Path.cwd(), output_dir)
323
338
 
@@ -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,53 @@
1
+ "``mmif-python`` version","Target MMIF Specification"
2
+ `1.5.2 <https://pypi.org/project/mmif-python/1.5.2/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
3
+ `1.5.1 <https://pypi.org/project/mmif-python/1.5.1/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
4
+ `1.5.0 <https://pypi.org/project/mmif-python/1.5.0/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
5
+ `1.4.0 <https://pypi.org/project/mmif-python/1.4.0/>`__,`1.1.1 <https://mmif.clams.ai/1.1.1/>`__
6
+ `1.3.1 <https://pypi.org/project/mmif-python/1.3.1/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
7
+ `1.3.0 <https://pypi.org/project/mmif-python/1.3.0/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
8
+ `1.2.1 <https://pypi.org/project/mmif-python/1.2.1/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
9
+ `1.2.0 <https://pypi.org/project/mmif-python/1.2.0/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
10
+ `1.1.2 <https://pypi.org/project/mmif-python/1.1.2/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
11
+ `1.1.1 <https://pypi.org/project/mmif-python/1.1.1/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
12
+ `1.1.0 <https://pypi.org/project/mmif-python/1.1.0/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
13
+ `1.0.19 <https://pypi.org/project/mmif-python/1.0.19/>`__,`1.0.5 <https://mmif.clams.ai/1.0.5/>`__
14
+ `1.0.18 <https://pypi.org/project/mmif-python/1.0.18/>`__,`1.0.5 <https://mmif.clams.ai/1.0.5/>`__
15
+ `1.0.17 <https://pypi.org/project/mmif-python/1.0.17/>`__,`1.0.5 <https://mmif.clams.ai/1.0.5/>`__
16
+ `1.0.16 <https://pypi.org/project/mmif-python/1.0.16/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
17
+ `1.0.15 <https://pypi.org/project/mmif-python/1.0.15/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
18
+ `1.0.14 <https://pypi.org/project/mmif-python/1.0.14/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
19
+ `1.0.13 <https://pypi.org/project/mmif-python/1.0.13/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
20
+ `1.0.12 <https://pypi.org/project/mmif-python/1.0.12/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
21
+ `1.0.11 <https://pypi.org/project/mmif-python/1.0.11/>`__,`1.0.4 <https://mmif.clams.ai/1.0.4/>`__
22
+ `1.0.10 <https://pypi.org/project/mmif-python/1.0.10/>`__,`1.0.2 <https://mmif.clams.ai/1.0.2/>`__
23
+ `1.0.9 <https://pypi.org/project/mmif-python/1.0.9/>`__,`1.1.0 <https://mmif.clams.ai/1.1.0/>`__
24
+ `1.0.8 <https://pypi.org/project/mmif-python/1.0.8/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
25
+ `1.0.7 <https://pypi.org/project/mmif-python/1.0.7/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
26
+ `1.0.6 <https://pypi.org/project/mmif-python/1.0.6/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
27
+ `1.0.5 <https://pypi.org/project/mmif-python/1.0.5/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
28
+ `1.0.4 <https://pypi.org/project/mmif-python/1.0.4/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
29
+ `1.0.3 <https://pypi.org/project/mmif-python/1.0.3/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
30
+ `1.0.2 <https://pypi.org/project/mmif-python/1.0.2/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
31
+ `1.0.1 <https://pypi.org/project/mmif-python/1.0.1/>`__,`1.0.0 <https://mmif.clams.ai/1.0.0/>`__
32
+ `1.0.0 <https://pypi.org/project/mmif-python/1.0.0/>`__,`0.5.0 <https://mmif.clams.ai/0.5.0/>`__
33
+ `0.5.2 <https://pypi.org/project/mmif-python/0.5.2/>`__,`0.5.0 <https://mmif.clams.ai/0.5.0/>`__
34
+ `0.5.1 <https://pypi.org/project/mmif-python/0.5.1/>`__,`0.5.0 <https://mmif.clams.ai/0.5.0/>`__
35
+ `0.5.0 <https://pypi.org/project/mmif-python/0.5.0/>`__,`0.5.0 <https://mmif.clams.ai/0.5.0/>`__
36
+ `0.4.8 <https://pypi.org/project/mmif-python/0.4.8/>`__,`0.4.2 <https://mmif.clams.ai/0.4.2/>`__
37
+ `0.4.7 <https://pypi.org/project/mmif-python/0.4.7/>`__,`0.4.1 <https://mmif.clams.ai/0.4.1/>`__
38
+ `0.4.6 <https://pypi.org/project/mmif-python/0.4.6/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
39
+ `0.4.5 <https://pypi.org/project/mmif-python/0.4.5/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
40
+ `0.4.4 <https://pypi.org/project/mmif-python/0.4.4/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
41
+ `0.4.3 <https://pypi.org/project/mmif-python/0.4.3/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
42
+ `0.4.2 <https://pypi.org/project/mmif-python/0.4.2/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
43
+ `0.4.1 <https://pypi.org/project/mmif-python/0.4.1/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
44
+ `0.4.0 <https://pypi.org/project/mmif-python/0.4.0/>`__,`0.4.0 <https://mmif.clams.ai/0.4.0/>`__
45
+ `0.3.5 <https://pypi.org/project/mmif-python/0.3.5/>`__,`0.3.1 <https://mmif.clams.ai/0.3.1/>`__
46
+ `0.3.4 <https://pypi.org/project/mmif-python/0.3.4/>`__,`0.3.1 <https://mmif.clams.ai/0.3.1/>`__
47
+ `0.3.3 <https://pypi.org/project/mmif-python/0.3.3/>`__,`0.3.1 <https://mmif.clams.ai/0.3.1/>`__
48
+ `0.3.2 <https://pypi.org/project/mmif-python/0.3.2/>`__,`0.3.1 <https://mmif.clams.ai/0.3.1/>`__
49
+ `0.3.1 <https://pypi.org/project/mmif-python/0.3.1/>`__,`0.3.0 <https://mmif.clams.ai/0.3.0/>`__
50
+ `0.3.0 <https://pypi.org/project/mmif-python/0.3.0/>`__,`0.3.0 <https://mmif.clams.ai/0.3.0/>`__
51
+ `0.2.2 <https://pypi.org/project/mmif-python/0.2.2/>`__,`0.2.1 <https://mmif.clams.ai/0.2.1/>`__
52
+ `0.2.1 <https://pypi.org/project/mmif-python/0.2.1/>`__,`0.2.1 <https://mmif.clams.ai/0.2.1/>`__
53
+ `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
 
@@ -82,6 +82,25 @@ class View(MmifObject):
82
82
  self._required_attributes = ["id", "metadata", "annotations"]
83
83
  super().__init__(view_obj)
84
84
  self._fix_old_short_ids()
85
+ self._rebuild_id_counts()
86
+
87
+ def _rebuild_id_counts(self):
88
+ """
89
+ Re-seed the per-view auto-ID counters from the annotations loaded during
90
+ deserialization.
91
+
92
+ ``_id_counts`` is not serialized, so a view read back from JSON starts
93
+ with an empty counter. Without this, the next :meth:`new_annotation`
94
+ regenerates an ID (e.g. ``an_1``) that already exists in the
95
+ deserialized view and :meth:`AnnotationsList.append` raises ``KeyError``.
96
+ See issue #393.
97
+ """
98
+ for annotation in self.annotations:
99
+ local_id = annotation.id.split(self.id_delimiter)[-1]
100
+ prefix, _, num = local_id.rpartition('_')
101
+ if prefix and num.isdigit():
102
+ self._id_counts[prefix] = max(
103
+ self._id_counts.get(prefix, 0), int(num))
85
104
 
86
105
  def _fix_old_short_ids(self):
87
106
  """
@@ -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.2
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
@@ -61,8 +63,11 @@ mmif_python.egg-info/SOURCES.txt
61
63
  mmif_python.egg-info/dependency_links.txt
62
64
  mmif_python.egg-info/entry_points.txt
63
65
  mmif_python.egg-info/requires.txt
66
+ mmif_python.egg-info/scm_file_list.json
67
+ mmif_python.egg-info/scm_version.json
64
68
  mmif_python.egg-info/top_level.txt
65
69
  tests/black-2997fps.mp4
70
+ tests/black-2997fps.webm
66
71
  tests/mmif_examples.py
67
72
  tests/test_serialize.py
68
73
  tests/test_specver.py
@@ -0,0 +1,95 @@
1
+ {
2
+ "files": [
3
+ "README.md",
4
+ "LICENSE",
5
+ "pyproject.toml",
6
+ "CHANGELOG.md",
7
+ "CONTRIBUTING.md",
8
+ ".gitignore",
9
+ "build-tools/publish.py",
10
+ "build-tools/prep_release.py",
11
+ "build-tools/build.py",
12
+ "build-tools/docs.py",
13
+ "build-tools/clean.py",
14
+ "build-tools/test.py",
15
+ "documentation/target-versions.csv",
16
+ "documentation/consumer-tutorial.rst",
17
+ "documentation/target-versions.rst",
18
+ "documentation/index.rst",
19
+ "documentation/introduction.rst",
20
+ "documentation/cli.rst",
21
+ "documentation/plugins.rst",
22
+ "documentation/summarizer.rst",
23
+ "documentation/conf.py",
24
+ "documentation/_mmif_example_builder/__init__.py",
25
+ "documentation/_templates/versions.html",
26
+ "mmif/__init__.py",
27
+ "mmif/vocabulary/__init__.py",
28
+ "mmif/vocabulary/base_types.py",
29
+ "mmif/vocabulary/document_types.py",
30
+ "mmif/vocabulary/annotation_types.py",
31
+ "mmif/utils/__init__.py",
32
+ "mmif/utils/text_document_helper.py",
33
+ "mmif/utils/timeunit_helper.py",
34
+ "mmif/utils/workflow_helper.py",
35
+ "mmif/utils/video_document_helper.py",
36
+ "mmif/utils/sequence_helper.py",
37
+ "mmif/utils/summarizer/__init__.py",
38
+ "mmif/utils/summarizer/config.py",
39
+ "mmif/utils/summarizer/utils.py",
40
+ "mmif/utils/summarizer/summary.py",
41
+ "mmif/utils/summarizer/graph.py",
42
+ "mmif/utils/summarizer/nodes.py",
43
+ "mmif/utils/cli/__init__.py",
44
+ "mmif/utils/cli/source.py",
45
+ "mmif/utils/cli/rewind.py",
46
+ "mmif/utils/cli/summarize.py",
47
+ "mmif/utils/cli/describe.py",
48
+ "mmif/serialize/model.py",
49
+ "mmif/serialize/__init__.py",
50
+ "mmif/serialize/view.py",
51
+ "mmif/serialize/annotation.py",
52
+ "mmif/serialize/mmif.py",
53
+ "mmif/res/__init__.py",
54
+ "mmif/res/mmif.json",
55
+ "mmif/ver/__init__.py",
56
+ "tests/mmif_examples.py",
57
+ "tests/test_specver.py",
58
+ "tests/test_utils.py",
59
+ "tests/test_utils_cli.py",
60
+ "tests/test_vocab.py",
61
+ "tests/testsrc-2997fps-ptsoffset.mp4",
62
+ "tests/test_serialize.py",
63
+ "tests/black-2997fps.mp4",
64
+ "tests/black-2997fps.webm",
65
+ "tests/mmif-examples/README.md",
66
+ "tests/mmif-examples/examples.json",
67
+ "tests/mmif-examples/1.0.5-old-shortid.json",
68
+ "tests/mmif-examples/swt-1.0.mmif",
69
+ "tests/mmif-examples/segmenter-kaldi-ner/raw.json",
70
+ "tests/mmif-examples/everything/raw.json",
71
+ "tests/mmif-examples/others/video-transcript.json",
72
+ "tests/mmif-examples/others/image-box-ocr-timex.json",
73
+ "tests/mmif-examples/others/README.md",
74
+ "tests/mmif-examples/others/video.json",
75
+ "tests/mmif-examples/others/video-transcript-demux-fa.json",
76
+ "tests/mmif-examples/others/image.json",
77
+ "tests/mmif-examples/others/video-transcript-sentencesplit.json",
78
+ "tests/mmif-examples/others/video-transcript-demux-fa.short.json",
79
+ "tests/mmif-examples/others/slate-detect.json",
80
+ "tests/mmif-examples/others/image-box.json",
81
+ "tests/mmif-examples/others/slate-reco.json",
82
+ "tests/mmif-examples/others/video-transcript-demux.json",
83
+ "tests/mmif-examples/others/image-box-ocr.json",
84
+ "tests/mmif-examples/others/video-demux.json",
85
+ "tests/mmif-examples/bars-tones-slates/raw.json",
86
+ "tests/mmif-examples/east-tesseract-typing/raw.json",
87
+ "mmif_docloc_http/__init__.py",
88
+ ".github/workflows/issue-close.yml",
89
+ ".github/workflows/codecov.yml",
90
+ ".github/workflows/release-check.yml",
91
+ ".github/workflows/issue-infra-project.yml",
92
+ ".github/workflows/issue-assign.yml",
93
+ ".github/workflows/publish.yml"
94
+ ]
95
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.5.2",
3
+ "distance": 0,
4
+ "node": "g1b794b4741b27b9999c5cd9fec00d46bef6d5d8e",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-07"
8
+ }
@@ -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()
@@ -1422,7 +1451,57 @@ class TestDocument(unittest.TestCase):
1422
1451
  'author'))
1423
1452
  self.assertEqual('you', list(mmif_roundtrip2.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))[
1424
1453
  0].get_property('author'))
1425
-
1454
+
1455
+ def test_view_id_counts_rebuilt_on_deserialize(self):
1456
+ # regression for issue #393: `_id_counts` is not serialized, so after
1457
+ # deserialization the next generated id must not collide with an existing
1458
+ # one already present in the view.
1459
+ mmif = Mmif(validate=False)
1460
+ doc = Document(); doc.at_type = DocumentTypes.VideoDocument
1461
+ doc.id = 'd1'; doc.location = 'file:///v.mp4'
1462
+ mmif.add_document(doc)
1463
+ v = mmif.new_view()
1464
+ v.metadata.app = tester_appname
1465
+ a1 = v.new_annotation(AnnotationTypes.Annotation, document='d1', frameCount=10)
1466
+ self.assertTrue(a1.id.endswith('an_1'))
1467
+ # round-trip: the reloaded view still contains `an_1` but starts with an
1468
+ # empty counter unless rebuilt
1469
+ v2 = Mmif(mmif.serialize())[v.id]
1470
+ a2 = v2.new_annotation(AnnotationTypes.Annotation, document='d1', duration=20)
1471
+ self.assertTrue(a2.id.endswith('an_2')) # must not regenerate an_1
1472
+
1473
+ def test_capital_annotation_no_collision_across_apps(self):
1474
+ # regression for issue #393: a downstream app re-deriving a document
1475
+ # property with a *different* value must not crash on serialize; the
1476
+ # distinct value is recorded as an additional capital annotation
1477
+ # (value-based de-dup preserves both assertions), not a colliding one.
1478
+ m = Mmif(validate=False)
1479
+ d = Document()
1480
+ d.at_type = DocumentTypes.VideoDocument
1481
+ d.id = 'd1'
1482
+ d.location = 'file:///v.mp4'
1483
+ m.add_document(d)
1484
+ v1 = m.new_view(); v1.metadata.app = 'http://app/producer'
1485
+ v1.new_annotation(AnnotationTypes.TimeFrame, label='bars')
1486
+ d.add_property('frameCount', 51248.0)
1487
+ d.add_property('duration', 1709977.0)
1488
+
1489
+ # downstream app loads it and re-derives a *different* duration value
1490
+ m2 = Mmif(m.serialize())
1491
+ d2 = m2['d1']
1492
+ v2 = m2.new_view(); v2.metadata.app = 'http://app/consumer'
1493
+ v2.new_annotation(AnnotationTypes.TimeFrame, label='caption')
1494
+ d2.add_property('frameCount', 51248) # same value -> de-duped away
1495
+ d2.add_property('duration', 1709975) # different value -> new annotation
1496
+ out = Mmif(m2.serialize()) # must not raise KeyError
1497
+
1498
+ caps = [a for view in out.views
1499
+ for a in view.get_annotations(AnnotationTypes.Annotation)]
1500
+ durations = {a.get_property('duration') for a in caps if 'duration' in a}
1501
+ # both distinct duration assertions coexist; neither is lost or overwritten
1502
+ self.assertEqual(2, len(caps))
1503
+ self.assertEqual({1709977.0, 1709975}, durations)
1504
+
1426
1505
  def test_capital_annotation_generation_viewfinder(self):
1427
1506
  mmif = Mmif(validate=False)
1428
1507
  for i in range(1, 3):
@@ -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