clams-python 1.8.0__tar.gz → 1.8.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 (103) hide show
  1. {clams_python-1.8.0 → clams_python-1.8.2}/CHANGELOG.md +30 -0
  2. {clams_python-1.8.0/clams_python.egg-info → clams_python-1.8.2}/PKG-INFO +2 -2
  3. {clams_python-1.8.0 → clams_python-1.8.2}/clams/app/__init__.py +50 -71
  4. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/__init__.py +18 -3
  5. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/templates/utl-tf/timeframe.py.template +8 -0
  6. {clams_python-1.8.0 → clams_python-1.8.2/clams_python.egg-info}/PKG-INFO +2 -2
  7. {clams_python-1.8.0 → clams_python-1.8.2}/clams_python.egg-info/requires.txt +1 -1
  8. clams_python-1.8.2/clams_python.egg-info/scm_version.json +8 -0
  9. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/target-versions.csv +2 -0
  10. {clams_python-1.8.0 → clams_python-1.8.2}/pyproject.toml +1 -1
  11. {clams_python-1.8.0 → clams_python-1.8.2}/tests/test_clamsapp.py +162 -14
  12. {clams_python-1.8.0 → clams_python-1.8.2}/tests/test_develop.py +15 -1
  13. clams_python-1.8.0/clams_python.egg-info/scm_version.json +0 -8
  14. {clams_python-1.8.0 → clams_python-1.8.2}/.github/workflows/codecov.yml +0 -0
  15. {clams_python-1.8.0 → clams_python-1.8.2}/.github/workflows/container.yml +0 -0
  16. {clams_python-1.8.0 → clams_python-1.8.2}/.github/workflows/containers.yml +0 -0
  17. {clams_python-1.8.0 → clams_python-1.8.2}/.github/workflows/issue-assign.yml +0 -0
  18. {clams_python-1.8.0 → clams_python-1.8.2}/.github/workflows/issue-close.yml +0 -0
  19. {clams_python-1.8.0 → clams_python-1.8.2}/.github/workflows/issue-infra-project.yml +0 -0
  20. {clams_python-1.8.0 → clams_python-1.8.2}/.github/workflows/publish.yml +0 -0
  21. {clams_python-1.8.0 → clams_python-1.8.2}/.github/workflows/release-check.yml +0 -0
  22. {clams_python-1.8.0 → clams_python-1.8.2}/.gitignore +0 -0
  23. {clams_python-1.8.0 → clams_python-1.8.2}/CONTRIBUTING.md +0 -0
  24. {clams_python-1.8.0 → clams_python-1.8.2}/LICENSE +0 -0
  25. {clams_python-1.8.0 → clams_python-1.8.2}/README.md +0 -0
  26. {clams_python-1.8.0 → clams_python-1.8.2}/build-tools/build.py +0 -0
  27. {clams_python-1.8.0 → clams_python-1.8.2}/build-tools/clean.py +0 -0
  28. {clams_python-1.8.0 → clams_python-1.8.2}/build-tools/docs.py +0 -0
  29. {clams_python-1.8.0 → clams_python-1.8.2}/build-tools/prep_release.py +0 -0
  30. {clams_python-1.8.0 → clams_python-1.8.2}/build-tools/publish.py +0 -0
  31. {clams_python-1.8.0 → clams_python-1.8.2}/build-tools/test.py +0 -0
  32. {clams_python-1.8.0 → clams_python-1.8.2}/clams/__init__.py +0 -0
  33. {clams_python-1.8.0 → clams_python-1.8.2}/clams/appmetadata/__init__.py +0 -0
  34. {clams_python-1.8.0 → clams_python-1.8.2}/clams/backends/__init__.py +0 -0
  35. {clams_python-1.8.0 → clams_python-1.8.2}/clams/backends/hf.py +0 -0
  36. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/templates/app/.dockerignore.template +0 -0
  37. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/templates/app/.gitignore.template +0 -0
  38. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/templates/app/Containerfile.template +0 -0
  39. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/templates/app/LICENSE.template +0 -0
  40. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/templates/app/README.md.template +0 -0
  41. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/templates/app/app.py.template +0 -0
  42. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/templates/app/cli.py.template +0 -0
  43. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/templates/app/metadata.py.template +0 -0
  44. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/templates/app/requirements.txt.template +0 -0
  45. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/templates/gha/for-clams-team.md.template +0 -0
  46. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/templates/gha/workflows/issue-apps-project.yml.template +0 -0
  47. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/templates/gha/workflows/issue-assign.yml.template +0 -0
  48. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/templates/gha/workflows/issue-close.yml.template +0 -0
  49. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/templates/gha/workflows/publish.yml.template +0 -0
  50. {clams_python-1.8.0 → clams_python-1.8.2}/clams/develop/templates/utl-tf/__init__.py.template +0 -0
  51. {clams_python-1.8.0 → clams_python-1.8.2}/clams/envelop/__init__.py +0 -0
  52. {clams_python-1.8.0 → clams_python-1.8.2}/clams/restify/__init__.py +0 -0
  53. {clams_python-1.8.0 → clams_python-1.8.2}/clams/serve/__init__.py +0 -0
  54. {clams_python-1.8.0 → clams_python-1.8.2}/clams/ver/__init__.py +0 -0
  55. {clams_python-1.8.0 → clams_python-1.8.2}/clams_python.egg-info/SOURCES.txt +0 -0
  56. {clams_python-1.8.0 → clams_python-1.8.2}/clams_python.egg-info/dependency_links.txt +0 -0
  57. {clams_python-1.8.0 → clams_python-1.8.2}/clams_python.egg-info/entry_points.txt +0 -0
  58. {clams_python-1.8.0 → clams_python-1.8.2}/clams_python.egg-info/scm_file_list.json +0 -0
  59. {clams_python-1.8.0 → clams_python-1.8.2}/clams_python.egg-info/top_level.txt +0 -0
  60. {clams_python-1.8.0 → clams_python-1.8.2}/container/Containerfile +0 -0
  61. {clams_python-1.8.0 → clams_python-1.8.2}/container/ffmpeg-hf.containerfile +0 -0
  62. {clams_python-1.8.0 → clams_python-1.8.2}/container/ffmpeg-hf4.containerfile +0 -0
  63. {clams_python-1.8.0 → clams_python-1.8.2}/container/ffmpeg-hf5.containerfile +0 -0
  64. {clams_python-1.8.0 → clams_python-1.8.2}/container/ffmpeg-tf2.containerfile +0 -0
  65. {clams_python-1.8.0 → clams_python-1.8.2}/container/ffmpeg-torch2.containerfile +0 -0
  66. {clams_python-1.8.0 → clams_python-1.8.2}/container/ffmpeg.containerfile +0 -0
  67. {clams_python-1.8.0 → clams_python-1.8.2}/container/generate_containers_yml.py +0 -0
  68. {clams_python-1.8.0 → clams_python-1.8.2}/container/hf.containerfile +0 -0
  69. {clams_python-1.8.0 → clams_python-1.8.2}/container/hf4.containerfile +0 -0
  70. {clams_python-1.8.0 → clams_python-1.8.2}/container/hf5.containerfile +0 -0
  71. {clams_python-1.8.0 → clams_python-1.8.2}/container/jdk8.containerfile +0 -0
  72. {clams_python-1.8.0 → clams_python-1.8.2}/container/opencv4-hf.containerfile +0 -0
  73. {clams_python-1.8.0 → clams_python-1.8.2}/container/opencv4-hf4.containerfile +0 -0
  74. {clams_python-1.8.0 → clams_python-1.8.2}/container/opencv4-hf5.containerfile +0 -0
  75. {clams_python-1.8.0 → clams_python-1.8.2}/container/opencv4-tf2.containerfile +0 -0
  76. {clams_python-1.8.0 → clams_python-1.8.2}/container/opencv4-torch2.containerfile +0 -0
  77. {clams_python-1.8.0 → clams_python-1.8.2}/container/opencv4.containerfile +0 -0
  78. {clams_python-1.8.0 → clams_python-1.8.2}/container/tf2.containerfile +0 -0
  79. {clams_python-1.8.0 → clams_python-1.8.2}/container/torch2.containerfile +0 -0
  80. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/app-baseclasses.rst +0 -0
  81. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/appdirectory.rst +0 -0
  82. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/appmetadata.rst +0 -0
  83. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/autodoc/clams.app.rst +0 -0
  84. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/autodoc/clams.appmetadata.rst +0 -0
  85. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/autodoc/clams.backends.rst +0 -0
  86. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/autodoc/clams.mmif_utils.rst +0 -0
  87. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/autodoc/clams.restify.rst +0 -0
  88. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/clamsapp.rst +0 -0
  89. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/cli.rst +0 -0
  90. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/conf.py +0 -0
  91. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/gpu-apps.rst +0 -0
  92. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/index.rst +0 -0
  93. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/input-output.rst +0 -0
  94. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/introduction.rst +0 -0
  95. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/modules.rst +0 -0
  96. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/runtime-params.rst +0 -0
  97. {clams_python-1.8.0 → clams_python-1.8.2}/documentation/tutorial.md +0 -0
  98. {clams_python-1.8.0 → clams_python-1.8.2}/setup.cfg +0 -0
  99. {clams_python-1.8.0 → clams_python-1.8.2}/tests/metadata.py +0 -0
  100. {clams_python-1.8.0 → clams_python-1.8.2}/tests/pillow-logo.png +0 -0
  101. {clams_python-1.8.0 → clams_python-1.8.2}/tests/test_backends_hf.py +0 -0
  102. {clams_python-1.8.0 → clams_python-1.8.2}/tests/test_envelope.py +0 -0
  103. {clams_python-1.8.0 → clams_python-1.8.2}/tests/test_promptable.py +0 -0
@@ -1,4 +1,34 @@
1
1
 
2
+ ## releasing 1.8.1 (2026-10-27)
3
+ ### Overview
4
+
5
+ Updates `mmif-python` to 1.6.0, with many small fixes for the consistency of MMIF files and CLAMS workflows.
6
+
7
+ ### Changes
8
+
9
+ * `mmif-python` is updated to 1.6.0 (https://github.com/clamsproject/clams-python/issues/314)
10
+ * view timestamps: consistently in UTC now, instead of local time (https://github.com/clamsproject/clams-python/issues/312)
11
+ * error views: fixed a bug where an error in a run overwrote the view of an earlier run of the same app (part of https://github.com/clamsproject/clams-python/pull/313, found during working on #312)
12
+ * document properties: fixed a bug where the (capital) `Annotation` for properties added to an existing document was attached to a wrong view, when the app produced no other annotations (https://github.com/clamsproject/clams-python/issues/314)
13
+ * `clams develop`: the `utl-tf` recipe is deprecated and no longer baked by default; its helpers are now provided by mmif-python 1.6.0 as `Mmif.get_annotations_of_labels()` and `mmif.utils.video_document_helper.iter_timeframe_images()` (https://github.com/clamsproject/clams-python/issues/314)
14
+
15
+
16
+ ## releasing 1.8.0 (2026-10-06)
17
+ ### Overview
18
+ Adds runtime parameters that cap the number of images per prompt in promptable apps, and updates `mmif-python` to 1.5.4.
19
+
20
+ ### Additions
21
+
22
+ * `maxImagesPerPrompt` and `imagesPerPromptMode`: new runtime parameters for promptable apps. A TimeFrame with more images than the cap is split into several prompts, which bounds GPU memory for long scenes ( https://github.com/clamsproject/clams-python/issues/306).
23
+
24
+ ### Changes
25
+
26
+ * `mmif-python` is updated to 1.5.4, which fixes image extraction from truncated or otherwise incomplete video files.
27
+ * (for contributors) Test-time type checking moved from `pytype` to `mypy` (https://github.com/clamsproject/clams-python/issues/285).
28
+
29
+
30
+
31
+
2
32
  ## releasing 1.7.4 (2026-09-01)
3
33
  ### Overview
4
34
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clams-python
3
- Version: 1.8.0
3
+ Version: 1.8.2
4
4
  Summary: A collection of APIs to develop CLAMS app for python
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: homepage, https://clams.ai
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
13
13
  Requires-Python: >=3.10
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
- Requires-Dist: mmif-python==1.5.4
16
+ Requires-Dist: mmif-python==1.7.0
17
17
  Requires-Dist: Flask>=2
18
18
  Requires-Dist: Flask-RESTful>=0.3.9
19
19
  Requires-Dist: gunicorn>=20
@@ -17,7 +17,9 @@ __all__ = ['ClamsApp', 'ClamsPromptableApp', 'ClamsHFPromptableApp',
17
17
 
18
18
  from typing import Union, Any, Optional, Dict, List, Mapping, Tuple, cast
19
19
 
20
- from mmif import Mmif, Document, DocumentTypes, View, AnnotationTypes
20
+ from mmif import (
21
+ Mmif, Document, DocumentTypes, View, AnnotationTypes, EmptyOutputWarning,
22
+ )
21
23
  from mmif.utils.video_document_helper import (
22
24
  SamplingMode, SAMPLING_MODE_DESCRIPTIONS, SAMPLING_MODE_DEFAULT,
23
25
  _sampling_mode,
@@ -53,10 +55,6 @@ falsy_values = [
53
55
  ]
54
56
 
55
57
 
56
- class EmptyOutputWarning(UserWarning):
57
- """Raised when an app's ``_annotate()`` produces no non-empty views."""
58
-
59
-
60
58
  class ImagesPerPromptMode(Enum):
61
59
  """
62
60
  How :py:meth:`ClamsPromptableApp.chunk_tasks` sizes the chunks of a
@@ -224,7 +222,6 @@ class ClamsApp(ABC):
224
222
  if not isinstance(mmif, Mmif):
225
223
  mmif, runtime_params = unwrap_if_envelope(mmif, runtime_params)
226
224
  mmif = Mmif(mmif)
227
- existing_view_ids = {view.id for view in mmif.views}
228
225
  issued_warnings: List[Union[Warning, warnings.WarningMessage]] = []
229
226
  for key in runtime_params:
230
227
  if key not in self.annotate_param_spec:
@@ -238,59 +235,43 @@ class ClamsApp(ABC):
238
235
  if sampling_mode_str is not None:
239
236
  _sampling_mode.set(SamplingMode(sampling_mode_str))
240
237
  t = datetime.now()
241
- with warnings.catch_warnings(record=True) as ws:
242
- annotated, cuda_profiler = self._profile_cuda_memory(self._annotate)(mmif, **refined)
243
- new_views = [v for v in annotated.views if v.id not in existing_view_ids]
244
- is_empty = lambda v: len(v.annotations) == 0 and not v.has_error() and not v.has_warnings()
245
- for v in new_views:
246
- if is_empty(v):
247
- del annotated.views._items[v.id] # no public API to remove a view (mmif-python)
248
- if all(is_empty(v) for v in new_views):
249
- warnings.warn(f'App "{self.metadata.identifier}" produced no annotations for this '
250
- f'input; the input may lack expected content, or there may genuinely '
251
- f'be nothing to annotate.', EmptyOutputWarning)
252
- if ws:
253
- issued_warnings.extend(ws)
254
- if issued_warnings:
255
- warnings_view = annotated.new_view()
256
- self.sign_view(warnings_view, refined)
257
- warnings_view.metadata.warnings = issued_warnings
258
- run_id = datetime.now()
259
- td = run_id - t
260
- runningTime = refined.get('runningTime', False)
261
- hwFetch = refined.get('hwFetch', False)
262
- runtime_recs: Dict[str, Any] = {}
263
- if hwFetch:
264
- import multiprocessing
265
- import platform, shutil, subprocess
266
- runtime_recs['cpu'] = f"{platform.machine()}, {multiprocessing.cpu_count()} cores"
267
- runtime_recs['cuda'] = []
268
- # Use cuda_profiler data if available, otherwise fallback to nvidia-smi
269
- if cuda_profiler:
270
- for gpu_name, mem_info in cuda_profiler.items():
271
- total_str = self._cuda_memory_to_str(mem_info['total'])
272
- available_str = self._cuda_memory_to_str(mem_info['available_before'])
273
- peak_str = self._cuda_memory_to_str(mem_info['peak'])
274
- runtime_recs['cuda'].append(
275
- f"{gpu_name}, {total_str} total, {available_str} available, {peak_str} peak used"
276
- )
277
- elif shutil.which('nvidia-smi'):
278
- for gpu in subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total', '--format=csv,noheader'],
279
- stdout=subprocess.PIPE).stdout.decode('utf-8').strip().split('\n'):
280
- name, mem = gpu.split(', ')
281
- runtime_recs['cuda'].append(self._cuda_device_name_concat(name, mem))
282
- for annotated_view in annotated.views:
283
- if annotated_view.id not in existing_view_ids and annotated_view.metadata.app == str(self.metadata.identifier):
284
- annotated_view.metadata.timestamp = run_id
285
- profiling_data: Dict[str, Any] = {}
286
- if runningTime:
287
- profiling_data['runningTime'] = str(td)
288
- if len(runtime_recs) > 0:
289
- profiling_data['hardware'] = runtime_recs
290
- if profiling_data:
291
- annotated_view.metadata.set_additional_property('appProfiling', profiling_data)
292
-
293
- return annotated.serialize(pretty=pretty, sanitize=True)
238
+ signature = View()
239
+ self.sign_view(signature, refined)
240
+ with mmif.set_producer(signature) as producer:
241
+ with warnings.catch_warnings(record=True) as ws:
242
+ annotated, cuda_profiler = self._profile_cuda_memory(self._annotate)(mmif, **refined)
243
+ producer.add_warnings(*issued_warnings, *ws)
244
+ td = datetime.now() - t
245
+ runningTime = refined.get('runningTime', False)
246
+ hwFetch = refined.get('hwFetch', False)
247
+ runtime_recs: Dict[str, Any] = {}
248
+ if hwFetch:
249
+ import multiprocessing
250
+ import platform, shutil, subprocess
251
+ runtime_recs['cpu'] = f"{platform.machine()}, {multiprocessing.cpu_count()} cores"
252
+ runtime_recs['cuda'] = []
253
+ # Use cuda_profiler data if available, otherwise fallback to nvidia-smi
254
+ if cuda_profiler:
255
+ for gpu_name, mem_info in cuda_profiler.items():
256
+ total_str = self._cuda_memory_to_str(mem_info['total'])
257
+ available_str = self._cuda_memory_to_str(mem_info['available_before'])
258
+ peak_str = self._cuda_memory_to_str(mem_info['peak'])
259
+ runtime_recs['cuda'].append(
260
+ f"{gpu_name}, {total_str} total, {available_str} available, {peak_str} peak used"
261
+ )
262
+ elif shutil.which('nvidia-smi'):
263
+ for gpu in subprocess.run(['nvidia-smi', '--query-gpu=name,memory.total', '--format=csv,noheader'],
264
+ stdout=subprocess.PIPE).stdout.decode('utf-8').strip().split('\n'):
265
+ name, mem = gpu.split(', ')
266
+ runtime_recs['cuda'].append(self._cuda_device_name_concat(name, mem))
267
+ profiling_data: Dict[str, Any] = {}
268
+ if runningTime:
269
+ profiling_data['runningTime'] = str(td)
270
+ if len(runtime_recs) > 0:
271
+ profiling_data['hardware'] = runtime_recs
272
+ if profiling_data:
273
+ producer.set_view_metadata('appProfiling', profiling_data)
274
+ return annotated.serialize(pretty=pretty, sanitize=True)
294
275
 
295
276
  @abstractmethod
296
277
  def _annotate(self, mmif: Mmif, _raw_parameters=None, **refined_parameters) -> Mmif:
@@ -391,7 +372,9 @@ class ClamsApp(ABC):
391
372
  """
392
373
  view.metadata.app = str(self.metadata.identifier)
393
374
  if self.metadata.app_tags:
394
- view.metadata.set_additional_property('appTags', list(self.metadata.app_tags))
375
+ # item assignment stores the tags in the declared `appTags` field
376
+ # when mmif-python has one, and as an additional property otherwise
377
+ view.metadata['appTags'] = list(self.metadata.app_tags)
395
378
  if self._RAW_PARAMS_KEY in runtime_conf:
396
379
  for k, v in runtime_conf.items():
397
380
  if k == self._RAW_PARAMS_KEY:
@@ -436,9 +419,11 @@ class ClamsApp(ABC):
436
419
 
437
420
  def set_error_view(self, mmif: Union[str, dict, Mmif], **runtime_conf: List[str]) -> Mmif:
438
421
  """
439
- A method to record an error instead of annotation results in the view
440
- this app generated. For logging purpose, the runtime parameters used
441
- when the error occurred must be passed as well.
422
+ A method to record an error instead of annotation results in a new
423
+ view appended to the input MMIF. Existing views are left untouched,
424
+ including views from earlier runs of the same app. For logging
425
+ purpose, the runtime parameters used when the error occurred must be
426
+ passed as well.
442
427
 
443
428
  :param mmif: input MMIF object
444
429
  :param runtime_conf: parameters passed to annotate when the app encountered the error
@@ -448,14 +433,8 @@ class ClamsApp(ABC):
448
433
  if isinstance(mmif, (bytes, str, dict)):
449
434
  mmif, runtime_conf = unwrap_if_envelope(mmif, runtime_conf)
450
435
  mmif = Mmif(mmif)
451
- error_view: Optional[View] = None
452
- for view in reversed(mmif.views):
453
- if view.metadata.app == str(self.metadata.identifier):
454
- error_view = view
455
- break
456
- if error_view is None:
457
- error_view = mmif.new_view()
458
- self.sign_view(error_view, runtime_conf)
436
+ error_view = mmif.new_view()
437
+ self.sign_view(error_view, runtime_conf)
459
438
  exc_info = sys.exc_info()
460
439
  error_view.set_error(f'{exc_info[0]}: {exc_info[1]}',
461
440
  '\t\n'.join(traceback.format_tb(exc_info[2])))
@@ -2,12 +2,15 @@ import argparse
2
2
  import pathlib
3
3
  import re
4
4
  import shutil
5
+ import warnings
5
6
  from string import Template
6
7
  from typing import List
7
8
 
8
9
  import clams
9
10
 
10
11
  update_tmp_suffix = '.tmp'
12
+ # Recipes with a ``deprecated`` message are left out of the default recipe
13
+ # set and warn when baked on request.
11
14
  available_recipes = {
12
15
  'app': {
13
16
  'description': 'Skeleton code for a CLAMS app',
@@ -20,12 +23,18 @@ available_recipes = {
20
23
  'targetdir': '.github',
21
24
  },
22
25
  'utl-tf': {
23
- 'description': 'Local helper module for iterating TimeFrames and collecting per-TF frame tasks '
26
+ 'description': 'DEPRECATED. Local helper module for iterating TimeFrames and collecting per-TF frame tasks '
24
27
  '(baked into ``utils/timeframe.py``; backend-agnostic, safe to edit/delete)',
25
28
  'sourcedir': 'utl-tf',
26
29
  'targetdir': 'utils',
30
+ 'deprecated': 'The `utl-tf` recipe is deprecated and is no longer baked by default. mmif-python now '
31
+ 'provides its functions: use `Mmif.get_annotations_of_labels()` to filter TimeFrames by '
32
+ 'label, and `mmif.utils.video_document_helper.iter_timeframe_images()` to sample images '
33
+ 'per TimeFrame with the TimePoint each image came from.',
27
34
  },
28
35
  }
36
+ default_recipes = [name for name, recipe in available_recipes.items()
37
+ if 'deprecated' not in recipe]
29
38
 
30
39
 
31
40
  class CookieCutter(object):
@@ -37,7 +46,13 @@ class CookieCutter(object):
37
46
  if recipes:
38
47
  self.recipes = recipes
39
48
  else:
40
- self.recipes = list(available_recipes.keys())
49
+ self.recipes = list(default_recipes)
50
+ for recipe in self.recipes:
51
+ message = available_recipes.get(recipe, {}).get('deprecated')
52
+ if message:
53
+ # FutureWarning, unlike DeprecationWarning, is shown to CLI
54
+ # users by default
55
+ warnings.warn(message, FutureWarning, stacklevel=2)
41
56
 
42
57
  def tokenize_rawname(self):
43
58
  word_pat = re.compile('[A-Z][a-z]+|[0-9A-Z]+(?=[A-Z][a-z])|[0-9A-Z]{2,}|[a-z0-9]{2,}|[a-zA-Z0-9]')
@@ -158,7 +173,7 @@ def prep_argparser(**kwargs):
158
173
  action='store',
159
174
  nargs='+',
160
175
  default=[],
161
- help=f"Pick recipes to bake. DEFAULT: {list(available_recipes.keys())}"
176
+ help=f"Pick recipes to bake. DEFAULT: {default_recipes}"
162
177
  )
163
178
  parser.add_argument(
164
179
  '-n', '--name',
@@ -1,6 +1,14 @@
1
1
  """
2
2
  TimeFrame iteration / frame-sampling helpers, local to this app.
3
3
 
4
+ DEPRECATED: mmif-python now provides these functions. Use
5
+ ``Mmif.get_annotations_of_labels()`` in place of
6
+ :func:`iter_timeframes`, and
7
+ ``mmif.utils.video_document_helper.iter_timeframe_images()`` in place of
8
+ :func:`collect_timeframes_of_interest` (it yields
9
+ ``(time_frame, images, timepoint_ids)``, and wrapping those into
10
+ ``clams.app.PromptTask`` is left to the app).
11
+
4
12
  Generated by ``clams develop -r utl-tf``. The code in this file is part
5
13
  of your app, not the SDK; edit it freely, refactor as needed, or delete
6
14
  the whole file if your app does not iterate TimeFrame annotations.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clams-python
3
- Version: 1.8.0
3
+ Version: 1.8.2
4
4
  Summary: A collection of APIs to develop CLAMS app for python
5
5
  License-Expression: Apache-2.0
6
6
  Project-URL: homepage, https://clams.ai
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
13
13
  Requires-Python: >=3.10
14
14
  Description-Content-Type: text/markdown
15
15
  License-File: LICENSE
16
- Requires-Dist: mmif-python==1.5.4
16
+ Requires-Dist: mmif-python==1.7.0
17
17
  Requires-Dist: Flask>=2
18
18
  Requires-Dist: Flask-RESTful>=0.3.9
19
19
  Requires-Dist: gunicorn>=20
@@ -1,4 +1,4 @@
1
- mmif-python==1.5.4
1
+ mmif-python==1.7.0
2
2
  Flask>=2
3
3
  Flask-RESTful>=0.3.9
4
4
  gunicorn>=20
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "1.8.2",
3
+ "distance": 0,
4
+ "node": "gde113a8698841b5578249f509461bf7e4b00d521",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-11-01"
8
+ }
@@ -1,4 +1,6 @@
1
1
  "``clams-python`` version","``mmif-python`` version","Target MMIF Specification"
2
+ `1.8.2 <https://pypi.org/project/clams-python/1.8.2/>`__,`1.7.0 <https://pypi.org/project/mmif-python/1.7.0/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
3
+ `1.8.1 <https://pypi.org/project/clams-python/1.8.1/>`__,`1.6.0 <https://pypi.org/project/mmif-python/1.6.0/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
2
4
  `1.8.0 <https://pypi.org/project/clams-python/1.8.0/>`__,`1.5.4 <https://pypi.org/project/mmif-python/1.5.4/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
3
5
  `1.7.4 <https://pypi.org/project/clams-python/1.7.4/>`__,`1.5.3 <https://pypi.org/project/mmif-python/1.5.3/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
4
6
  `1.7.3 <https://pypi.org/project/clams-python/1.7.3/>`__,`1.5.1 <https://pypi.org/project/mmif-python/1.5.1/>`__,`1.2.0 <https://mmif.clams.ai/1.2.0/>`__
@@ -19,7 +19,7 @@ classifiers = [
19
19
  "Programming Language :: Python :: 3 :: Only",
20
20
  ]
21
21
  dependencies = [
22
- "mmif-python==1.5.4",
22
+ "mmif-python==1.7.0",
23
23
  "Flask>=2",
24
24
  "Flask-RESTful>=0.3.9",
25
25
  "gunicorn>=20",
@@ -5,11 +5,16 @@ import sys
5
5
  import tempfile
6
6
  import unittest
7
7
  import warnings
8
+ from datetime import datetime, timezone
8
9
  from typing import Union
10
+ from unittest.mock import patch
9
11
 
10
12
  import jsonschema
11
13
  import pytest
12
- from mmif import Mmif, Document, DocumentTypes, AnnotationTypes, View, __specver__
14
+ from mmif import (
15
+ Mmif, Document, DocumentTypes, AnnotationTypes, View, EmptyOutputWarning,
16
+ __specver__,
17
+ )
13
18
  from mmif.serialize.mmif import ViewsList
14
19
 
15
20
  import clams.app
@@ -334,11 +339,6 @@ class TestClamsApp(unittest.TestCase):
334
339
  self.assertEqual(len(v.metadata.parameters), 1) # 'pretty` parameter was passed
335
340
  out_mmif = Mmif(self.app.annotate(out_mmif))
336
341
  self.assertEqual(len(out_mmif.views), 4)
337
- views = list(out_mmif.views)
338
- # insertion order is kept
339
- self.assertEqual(views[0].metadata.timestamp, views[1].metadata.timestamp)
340
- self.assertEqual(views[2].metadata.timestamp, views[3].metadata.timestamp)
341
- self.assertTrue(views[1].metadata.timestamp < views[2].metadata.timestamp)
342
342
 
343
343
  def test_run_id(self):
344
344
  # first run
@@ -360,6 +360,31 @@ class TestClamsApp(unittest.TestCase):
360
360
  else:
361
361
  self.assertEqual(second_timestamp, view.metadata.timestamp)
362
362
 
363
+ def test_run_id_reuses_mmif_timestamp(self):
364
+ # the run ID is the timestamp mmif-python assigned to the first view
365
+ # of the run; clams-python does not generate its own
366
+ sentinel = datetime(2001, 2, 3, 4, 5, 6, tzinfo=timezone.utc)
367
+ original_annotate = self.app._annotate
368
+
369
+ def _annotate(mmif, **kwargs):
370
+ out = original_annotate(mmif, **kwargs)
371
+ next(reversed(out.views)).metadata.timestamp = sentinel
372
+ return out
373
+
374
+ # serialize the sentinel the same way mmif-python does
375
+ ref = ExampleInputMMIF.get_rawmmif()
376
+ ref.new_view().metadata.timestamp = sentinel
377
+ expected = next(reversed(
378
+ Mmif(ref.serialize(), validate=False).views)).metadata.timestamp
379
+ with patch.object(self.app, '_annotate', new=_annotate):
380
+ out_mmif = Mmif(self.app.annotate(self.in_mmif))
381
+ app_views = [v for v in out_mmif.views
382
+ if v.metadata.app == str(self.app.metadata.identifier)]
383
+ # one contentful view and one warning view
384
+ self.assertEqual(len(app_views), 2)
385
+ for view in app_views:
386
+ self.assertEqual(expected, view.metadata.timestamp)
387
+
363
388
  def test_annotate_returns_invalid_mmif(self):
364
389
  m = Mmif(self.in_mmif)
365
390
  v = m.new_view()
@@ -430,21 +455,38 @@ class TestClamsApp(unittest.TestCase):
430
455
 
431
456
  def test_error_handling(self):
432
457
  params = {'raise_error': ['true'], 'pretty': ['true']}
433
- in_mmif = Mmif(self.in_mmif)
434
458
  try:
435
- out_mmif = self.app.annotate(in_mmif, **params)
459
+ out_mmif = self.app.annotate(self.in_mmif, **params)
436
460
  except Exception as e:
437
- out_mmif_from_str = self.app.set_error_view(self.in_mmif, **params)
438
- out_mmif_from_mmif = self.app.set_error_view(in_mmif, **params)
439
- self.assertEqual(
440
- out_mmif_from_mmif.views.get_last_contentful_view(),
441
- out_mmif_from_str.views.get_last_contentful_view())
442
- out_mmif = out_mmif_from_str
461
+ out_mmif = self.app.set_error_view(self.in_mmif, **params)
443
462
  self.assertIsNotNone(out_mmif)
463
+ self.assertEqual(len(out_mmif.views), 1)
444
464
  last_view: View = next(reversed(out_mmif.views))
445
465
  self.assertEqual(len(last_view.metadata.contains), 0)
446
466
  self.assertEqual(len(last_view.metadata.error), 2)
447
467
 
468
+ def test_error_view_keeps_previous_run(self):
469
+ # an error in a later run of the same app must not overwrite
470
+ # the views from an earlier, successful run
471
+ first = Mmif(self.app.annotate(self.in_mmif))
472
+ params = {'raise_error': ['true']}
473
+ try:
474
+ self.app.annotate(first.serialize(), **params)
475
+ except Exception:
476
+ out_mmif = self.app.set_error_view(first.serialize(), **params)
477
+ self.assertEqual(len(out_mmif.views), len(first.views) + 1)
478
+ for before, after in zip(first.views, out_mmif.views):
479
+ self.assertEqual(before.id, after.id)
480
+ self.assertFalse(after.has_error())
481
+ self.assertEqual(len(before.annotations), len(after.annotations))
482
+ self.assertEqual(before.metadata.timestamp,
483
+ after.metadata.timestamp)
484
+ error_view: View = next(reversed(out_mmif.views))
485
+ self.assertTrue(error_view.has_error())
486
+ self.assertEqual(len(error_view.annotations), 0)
487
+ self.assertNotIn(error_view.metadata.timestamp,
488
+ [v.metadata.timestamp for v in first.views])
489
+
448
490
  def test_empty_view_becomes_warning_view(self):
449
491
  class EmptyClamsApp(clams.app.ClamsApp):
450
492
  def _appmetadata(self):
@@ -479,6 +521,65 @@ class TestClamsApp(unittest.TestCase):
479
521
  self.assertEqual(len(view.annotations), 1)
480
522
  self.assertFalse(view.has_warnings())
481
523
 
524
+ def _in_mmif_with_earlier_view(self):
525
+ in_mmif = Mmif(self.in_mmif)
526
+ doc_id = next(iter(in_mmif.documents)).id
527
+ earlier = in_mmif.new_view()
528
+ earlier.metadata.app = 'http://apps.clams.ai/earlier-app/v1'
529
+ earlier.new_contain(AnnotationTypes.TimeFrame, document=doc_id)
530
+ earlier.new_annotation(AnnotationTypes.TimeFrame, start=0, end=1)
531
+ return in_mmif.serialize()
532
+
533
+ def test_document_properties_go_to_a_new_signed_view(self):
534
+ class DocPropClamsApp(clams.app.ClamsApp):
535
+ def _appmetadata(self):
536
+ pass
537
+ def _annotate(self, mmif, **kwargs):
538
+ next(iter(mmif.documents)).add_property('duration', 1000)
539
+ return mmif
540
+ app = DocPropClamsApp()
541
+ in_str = self._in_mmif_with_earlier_view()
542
+ out_str = app.annotate(in_str)
543
+ self.assertEqual(json.loads(in_str)['views'][0],
544
+ json.loads(out_str)['views'][0])
545
+ out_views = list(Mmif(out_str).views)
546
+ self.assertEqual(len(out_views), 2)
547
+ self.assertEqual(out_views[1].metadata.app,
548
+ str(app.metadata.identifier))
549
+ records = list(out_views[1].get_annotations(AnnotationTypes.Annotation))
550
+ self.assertEqual(len(records), 1)
551
+ self.assertEqual(records[0].get_property('duration'), 1000)
552
+ self.assertFalse(out_views[1].has_warnings())
553
+
554
+ def test_earlier_views_are_written_unchanged(self):
555
+ in_str = self._in_mmif_with_earlier_view()
556
+ out_str = self.app.annotate(in_str)
557
+ self.assertEqual(json.loads(in_str)['views'][0],
558
+ json.loads(out_str)['views'][0])
559
+
560
+ def test_failed_run_leaves_mmif_reusable(self):
561
+ class FlakyClamsApp(clams.app.ClamsApp):
562
+ attempts = 0
563
+ def _appmetadata(self):
564
+ pass
565
+ def _annotate(self, mmif, **kwargs):
566
+ self.attempts += 1
567
+ if self.attempts == 1:
568
+ raise ValueError('first attempt fails')
569
+ new_view = mmif.new_view()
570
+ self.sign_view(new_view, kwargs)
571
+ new_view.new_contain(AnnotationTypes.TimeFrame)
572
+ new_view.new_annotation(AnnotationTypes.TimeFrame, start=0, end=1)
573
+ return mmif
574
+ app = FlakyClamsApp()
575
+ in_mmif = Mmif(self.in_mmif)
576
+ with self.assertRaises(ValueError):
577
+ app.annotate(in_mmif)
578
+ self.assertEqual(len(Mmif(app.annotate(in_mmif)).views), 1)
579
+
580
+ def test_empty_output_warning_is_importable_from_clams(self):
581
+ self.assertIs(clams.app.EmptyOutputWarning, EmptyOutputWarning)
582
+
482
583
  def test_self_reported_error_view_is_not_pruned(self):
483
584
  class ErroringClamsApp(clams.app.ClamsApp):
484
585
  def _appmetadata(self):
@@ -494,6 +595,53 @@ class TestClamsApp(unittest.TestCase):
494
595
  self.assertTrue(view.has_error())
495
596
  self.assertFalse(view.has_warnings())
496
597
 
598
+ def test_self_reported_warning_view_is_not_pruned(self):
599
+ class WarningClamsApp(clams.app.ClamsApp):
600
+ def _appmetadata(self):
601
+ pass
602
+ def _annotate(self, mmif, **kwargs):
603
+ new_view = mmif.new_view()
604
+ self.sign_view(new_view, kwargs)
605
+ new_view.metadata.warnings = ['simulated warning']
606
+ return mmif
607
+ out_mmif = Mmif(WarningClamsApp().annotate(self.in_mmif))
608
+ self.assertEqual(len(out_mmif.views), 1)
609
+ view = next(iter(out_mmif.views))
610
+ self.assertEqual(view.metadata.warnings, ['simulated warning'])
611
+
612
+ def test_document_properties_stay_in_own_view(self):
613
+ # an app whose only output is document properties keeps its own view;
614
+ # the generated `Annotation`s must not go to an earlier app's view
615
+ class DocPropsClamsApp(clams.app.ClamsApp):
616
+ def _appmetadata(self):
617
+ pass
618
+ def _annotate(self, mmif, **kwargs):
619
+ new_view = mmif.new_view()
620
+ self.sign_view(new_view, kwargs)
621
+ mmif['v1'].add_property('duration', 1234)
622
+ mmif['t1'].add_property('author', 'me')
623
+ return mmif
624
+ in_mmif = Mmif(self.in_mmif)
625
+ earlier = in_mmif.new_view()
626
+ earlier.metadata.app = 'http://apps.clams.ai/earlier-app/v1'
627
+ earlier.new_annotation(AnnotationTypes.TimeFrame, start=0, end=10)
628
+ app = DocPropsClamsApp()
629
+ out_mmif = Mmif(app.annotate(in_mmif.serialize()))
630
+ self.assertEqual(
631
+ 0, len(list(out_mmif[earlier.id].get_annotations(AnnotationTypes.Annotation))))
632
+ own_views = [v for v in out_mmif.views
633
+ if v.metadata.app == str(app.metadata.identifier)]
634
+ self.assertEqual(len(own_views), 1)
635
+ self.assertFalse(own_views[0].has_warnings())
636
+ # one `Annotation` per document, even though `serialize()` converts
637
+ # the same pending properties a second time
638
+ anns = list(own_views[0].get_annotations(AnnotationTypes.Annotation))
639
+ self.assertEqual(len(anns), 2)
640
+ props = {a.get_property('document'):
641
+ {k: v for k, v in a.properties.items() if k not in ('id', 'document')}
642
+ for a in anns}
643
+ self.assertEqual({'v1': {'duration': 1234}, 't1': {'author': 'me'}}, props)
644
+
497
645
  def test_gpu_mem_fields_default_zero(self):
498
646
  """GPU memory fields default to 0."""
499
647
  metadata = AppMetadata(
@@ -7,6 +7,7 @@ import io
7
7
  import sys
8
8
  import tempfile
9
9
  import unittest
10
+ import warnings
10
11
  from pathlib import Path
11
12
  from unittest import mock
12
13
 
@@ -16,12 +17,25 @@ from clams.app import PromptTask
16
17
  from clams.develop import CookieCutter
17
18
 
18
19
 
20
+ class TestRecipeDefaults(unittest.TestCase):
21
+
22
+ def test_default_recipes_leave_out_deprecated(self):
23
+ with warnings.catch_warnings():
24
+ warnings.simplefilter('error')
25
+ cutter = CookieCutter(
26
+ name='app', outdir=tempfile.gettempdir(), recipes=[])
27
+ self.assertTrue(cutter.recipes)
28
+ self.assertNotIn('utl-tf', cutter.recipes)
29
+
30
+
19
31
  class TestUtlTfRecipe(unittest.TestCase):
20
32
 
21
33
  def setUp(self):
22
34
  tmp = tempfile.TemporaryDirectory()
23
35
  self.addCleanup(tmp.cleanup)
24
- with contextlib.redirect_stdout(io.StringIO()):
36
+ # `utl-tf` is deprecated, and asking for it warns
37
+ with self.assertWarns(FutureWarning), \
38
+ contextlib.redirect_stdout(io.StringIO()):
25
39
  CookieCutter(
26
40
  name='tfapp', outdir=tmp.name, recipes=['utl-tf']).bake()
27
41
  path = Path(tmp.name) / 'tfapp' / 'utils' / 'timeframe.py'
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "1.8.0",
3
- "distance": 0,
4
- "node": "gea09fcb6824996ccfe1eadbe534c55d7ddcc25a7",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-10-06"
8
- }
File without changes
File without changes
File without changes
File without changes