clams-python 1.8.0__tar.gz → 1.8.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 (103) hide show
  1. {clams_python-1.8.0 → clams_python-1.8.1}/CHANGELOG.md +16 -0
  2. {clams_python-1.8.0/clams_python.egg-info → clams_python-1.8.1}/PKG-INFO +2 -2
  3. {clams_python-1.8.0 → clams_python-1.8.1}/clams/app/__init__.py +33 -24
  4. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/__init__.py +18 -3
  5. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/templates/utl-tf/timeframe.py.template +8 -0
  6. {clams_python-1.8.0 → clams_python-1.8.1/clams_python.egg-info}/PKG-INFO +2 -2
  7. {clams_python-1.8.0 → clams_python-1.8.1}/clams_python.egg-info/requires.txt +1 -1
  8. clams_python-1.8.1/clams_python.egg-info/scm_version.json +8 -0
  9. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/target-versions.csv +1 -0
  10. {clams_python-1.8.0 → clams_python-1.8.1}/pyproject.toml +1 -1
  11. {clams_python-1.8.0 → clams_python-1.8.1}/tests/test_clamsapp.py +99 -13
  12. {clams_python-1.8.0 → clams_python-1.8.1}/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.1}/.github/workflows/codecov.yml +0 -0
  15. {clams_python-1.8.0 → clams_python-1.8.1}/.github/workflows/container.yml +0 -0
  16. {clams_python-1.8.0 → clams_python-1.8.1}/.github/workflows/containers.yml +0 -0
  17. {clams_python-1.8.0 → clams_python-1.8.1}/.github/workflows/issue-assign.yml +0 -0
  18. {clams_python-1.8.0 → clams_python-1.8.1}/.github/workflows/issue-close.yml +0 -0
  19. {clams_python-1.8.0 → clams_python-1.8.1}/.github/workflows/issue-infra-project.yml +0 -0
  20. {clams_python-1.8.0 → clams_python-1.8.1}/.github/workflows/publish.yml +0 -0
  21. {clams_python-1.8.0 → clams_python-1.8.1}/.github/workflows/release-check.yml +0 -0
  22. {clams_python-1.8.0 → clams_python-1.8.1}/.gitignore +0 -0
  23. {clams_python-1.8.0 → clams_python-1.8.1}/CONTRIBUTING.md +0 -0
  24. {clams_python-1.8.0 → clams_python-1.8.1}/LICENSE +0 -0
  25. {clams_python-1.8.0 → clams_python-1.8.1}/README.md +0 -0
  26. {clams_python-1.8.0 → clams_python-1.8.1}/build-tools/build.py +0 -0
  27. {clams_python-1.8.0 → clams_python-1.8.1}/build-tools/clean.py +0 -0
  28. {clams_python-1.8.0 → clams_python-1.8.1}/build-tools/docs.py +0 -0
  29. {clams_python-1.8.0 → clams_python-1.8.1}/build-tools/prep_release.py +0 -0
  30. {clams_python-1.8.0 → clams_python-1.8.1}/build-tools/publish.py +0 -0
  31. {clams_python-1.8.0 → clams_python-1.8.1}/build-tools/test.py +0 -0
  32. {clams_python-1.8.0 → clams_python-1.8.1}/clams/__init__.py +0 -0
  33. {clams_python-1.8.0 → clams_python-1.8.1}/clams/appmetadata/__init__.py +0 -0
  34. {clams_python-1.8.0 → clams_python-1.8.1}/clams/backends/__init__.py +0 -0
  35. {clams_python-1.8.0 → clams_python-1.8.1}/clams/backends/hf.py +0 -0
  36. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/templates/app/.dockerignore.template +0 -0
  37. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/templates/app/.gitignore.template +0 -0
  38. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/templates/app/Containerfile.template +0 -0
  39. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/templates/app/LICENSE.template +0 -0
  40. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/templates/app/README.md.template +0 -0
  41. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/templates/app/app.py.template +0 -0
  42. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/templates/app/cli.py.template +0 -0
  43. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/templates/app/metadata.py.template +0 -0
  44. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/templates/app/requirements.txt.template +0 -0
  45. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/templates/gha/for-clams-team.md.template +0 -0
  46. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/templates/gha/workflows/issue-apps-project.yml.template +0 -0
  47. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/templates/gha/workflows/issue-assign.yml.template +0 -0
  48. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/templates/gha/workflows/issue-close.yml.template +0 -0
  49. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/templates/gha/workflows/publish.yml.template +0 -0
  50. {clams_python-1.8.0 → clams_python-1.8.1}/clams/develop/templates/utl-tf/__init__.py.template +0 -0
  51. {clams_python-1.8.0 → clams_python-1.8.1}/clams/envelop/__init__.py +0 -0
  52. {clams_python-1.8.0 → clams_python-1.8.1}/clams/restify/__init__.py +0 -0
  53. {clams_python-1.8.0 → clams_python-1.8.1}/clams/serve/__init__.py +0 -0
  54. {clams_python-1.8.0 → clams_python-1.8.1}/clams/ver/__init__.py +0 -0
  55. {clams_python-1.8.0 → clams_python-1.8.1}/clams_python.egg-info/SOURCES.txt +0 -0
  56. {clams_python-1.8.0 → clams_python-1.8.1}/clams_python.egg-info/dependency_links.txt +0 -0
  57. {clams_python-1.8.0 → clams_python-1.8.1}/clams_python.egg-info/entry_points.txt +0 -0
  58. {clams_python-1.8.0 → clams_python-1.8.1}/clams_python.egg-info/scm_file_list.json +0 -0
  59. {clams_python-1.8.0 → clams_python-1.8.1}/clams_python.egg-info/top_level.txt +0 -0
  60. {clams_python-1.8.0 → clams_python-1.8.1}/container/Containerfile +0 -0
  61. {clams_python-1.8.0 → clams_python-1.8.1}/container/ffmpeg-hf.containerfile +0 -0
  62. {clams_python-1.8.0 → clams_python-1.8.1}/container/ffmpeg-hf4.containerfile +0 -0
  63. {clams_python-1.8.0 → clams_python-1.8.1}/container/ffmpeg-hf5.containerfile +0 -0
  64. {clams_python-1.8.0 → clams_python-1.8.1}/container/ffmpeg-tf2.containerfile +0 -0
  65. {clams_python-1.8.0 → clams_python-1.8.1}/container/ffmpeg-torch2.containerfile +0 -0
  66. {clams_python-1.8.0 → clams_python-1.8.1}/container/ffmpeg.containerfile +0 -0
  67. {clams_python-1.8.0 → clams_python-1.8.1}/container/generate_containers_yml.py +0 -0
  68. {clams_python-1.8.0 → clams_python-1.8.1}/container/hf.containerfile +0 -0
  69. {clams_python-1.8.0 → clams_python-1.8.1}/container/hf4.containerfile +0 -0
  70. {clams_python-1.8.0 → clams_python-1.8.1}/container/hf5.containerfile +0 -0
  71. {clams_python-1.8.0 → clams_python-1.8.1}/container/jdk8.containerfile +0 -0
  72. {clams_python-1.8.0 → clams_python-1.8.1}/container/opencv4-hf.containerfile +0 -0
  73. {clams_python-1.8.0 → clams_python-1.8.1}/container/opencv4-hf4.containerfile +0 -0
  74. {clams_python-1.8.0 → clams_python-1.8.1}/container/opencv4-hf5.containerfile +0 -0
  75. {clams_python-1.8.0 → clams_python-1.8.1}/container/opencv4-tf2.containerfile +0 -0
  76. {clams_python-1.8.0 → clams_python-1.8.1}/container/opencv4-torch2.containerfile +0 -0
  77. {clams_python-1.8.0 → clams_python-1.8.1}/container/opencv4.containerfile +0 -0
  78. {clams_python-1.8.0 → clams_python-1.8.1}/container/tf2.containerfile +0 -0
  79. {clams_python-1.8.0 → clams_python-1.8.1}/container/torch2.containerfile +0 -0
  80. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/app-baseclasses.rst +0 -0
  81. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/appdirectory.rst +0 -0
  82. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/appmetadata.rst +0 -0
  83. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/autodoc/clams.app.rst +0 -0
  84. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/autodoc/clams.appmetadata.rst +0 -0
  85. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/autodoc/clams.backends.rst +0 -0
  86. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/autodoc/clams.mmif_utils.rst +0 -0
  87. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/autodoc/clams.restify.rst +0 -0
  88. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/clamsapp.rst +0 -0
  89. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/cli.rst +0 -0
  90. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/conf.py +0 -0
  91. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/gpu-apps.rst +0 -0
  92. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/index.rst +0 -0
  93. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/input-output.rst +0 -0
  94. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/introduction.rst +0 -0
  95. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/modules.rst +0 -0
  96. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/runtime-params.rst +0 -0
  97. {clams_python-1.8.0 → clams_python-1.8.1}/documentation/tutorial.md +0 -0
  98. {clams_python-1.8.0 → clams_python-1.8.1}/setup.cfg +0 -0
  99. {clams_python-1.8.0 → clams_python-1.8.1}/tests/metadata.py +0 -0
  100. {clams_python-1.8.0 → clams_python-1.8.1}/tests/pillow-logo.png +0 -0
  101. {clams_python-1.8.0 → clams_python-1.8.1}/tests/test_backends_hf.py +0 -0
  102. {clams_python-1.8.0 → clams_python-1.8.1}/tests/test_envelope.py +0 -0
  103. {clams_python-1.8.0 → clams_python-1.8.1}/tests/test_promptable.py +0 -0
@@ -1,4 +1,20 @@
1
1
 
2
+ ## releasing 1.8.0 (2026-10-06)
3
+ ### Overview
4
+ Adds runtime parameters that cap the number of images per prompt in promptable apps, and updates `mmif-python` to 1.5.4.
5
+
6
+ ### Additions
7
+
8
+ * `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).
9
+
10
+ ### Changes
11
+
12
+ * `mmif-python` is updated to 1.5.4, which fixes image extraction from truncated or otherwise incomplete video files.
13
+ * (for contributors) Test-time type checking moved from `pytype` to `mypy` (https://github.com/clamsproject/clams-python/issues/285).
14
+
15
+
16
+
17
+
2
18
  ## releasing 1.7.4 (2026-09-01)
3
19
  ### Overview
4
20
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: clams-python
3
- Version: 1.8.0
3
+ Version: 1.8.1
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.6.0
17
17
  Requires-Dist: Flask>=2
18
18
  Requires-Dist: Flask-RESTful>=0.3.9
19
19
  Requires-Dist: gunicorn>=20
@@ -240,8 +240,13 @@ class ClamsApp(ABC):
240
240
  t = datetime.now()
241
241
  with warnings.catch_warnings(record=True) as ws:
242
242
  annotated, cuda_profiler = self._profile_cuda_memory(self._annotate)(mmif, **refined)
243
+ # turn pending document properties into `Annotation`s now, so a
244
+ # view whose only output is document properties is not empty;
245
+ # `serialize()` repeats this later, skipping values already written
246
+ annotated.generate_capital_annotations()
243
247
  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()
248
+ # an app-set error or warnings view has no annotations, but is kept
249
+ is_empty = lambda v: not (v.has_content() or v.has_error() or v.has_warnings())
245
250
  for v in new_views:
246
251
  if is_empty(v):
247
252
  del annotated.views._items[v.id] # no public API to remove a view (mmif-python)
@@ -255,8 +260,7 @@ class ClamsApp(ABC):
255
260
  warnings_view = annotated.new_view()
256
261
  self.sign_view(warnings_view, refined)
257
262
  warnings_view.metadata.warnings = issued_warnings
258
- run_id = datetime.now()
259
- td = run_id - t
263
+ td = datetime.now() - t
260
264
  runningTime = refined.get('runningTime', False)
261
265
  hwFetch = refined.get('hwFetch', False)
262
266
  runtime_recs: Dict[str, Any] = {}
@@ -279,16 +283,23 @@ class ClamsApp(ABC):
279
283
  stdout=subprocess.PIPE).stdout.decode('utf-8').strip().split('\n'):
280
284
  name, mem = gpu.split(', ')
281
285
  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):
286
+ new_app_views = [v for v in annotated.views
287
+ if v.id not in existing_view_ids
288
+ and v.metadata.app == str(self.metadata.identifier)]
289
+ # all views from this run share the timestamp that mmif-python
290
+ # assigned to the first of them, serving as the "run ID"
291
+ run_id = next((v.metadata.timestamp for v in new_app_views
292
+ if v.metadata.timestamp is not None), None)
293
+ for annotated_view in new_app_views:
294
+ if run_id is not None:
284
295
  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)
296
+ profiling_data: Dict[str, Any] = {}
297
+ if runningTime:
298
+ profiling_data['runningTime'] = str(td)
299
+ if len(runtime_recs) > 0:
300
+ profiling_data['hardware'] = runtime_recs
301
+ if profiling_data:
302
+ annotated_view.metadata.set_additional_property('appProfiling', profiling_data)
292
303
 
293
304
  return annotated.serialize(pretty=pretty, sanitize=True)
294
305
 
@@ -391,7 +402,9 @@ class ClamsApp(ABC):
391
402
  """
392
403
  view.metadata.app = str(self.metadata.identifier)
393
404
  if self.metadata.app_tags:
394
- view.metadata.set_additional_property('appTags', list(self.metadata.app_tags))
405
+ # item assignment stores the tags in the declared `appTags` field
406
+ # when mmif-python has one, and as an additional property otherwise
407
+ view.metadata['appTags'] = list(self.metadata.app_tags)
395
408
  if self._RAW_PARAMS_KEY in runtime_conf:
396
409
  for k, v in runtime_conf.items():
397
410
  if k == self._RAW_PARAMS_KEY:
@@ -436,9 +449,11 @@ class ClamsApp(ABC):
436
449
 
437
450
  def set_error_view(self, mmif: Union[str, dict, Mmif], **runtime_conf: List[str]) -> Mmif:
438
451
  """
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.
452
+ A method to record an error instead of annotation results in a new
453
+ view appended to the input MMIF. Existing views are left untouched,
454
+ including views from earlier runs of the same app. For logging
455
+ purpose, the runtime parameters used when the error occurred must be
456
+ passed as well.
442
457
 
443
458
  :param mmif: input MMIF object
444
459
  :param runtime_conf: parameters passed to annotate when the app encountered the error
@@ -448,14 +463,8 @@ class ClamsApp(ABC):
448
463
  if isinstance(mmif, (bytes, str, dict)):
449
464
  mmif, runtime_conf = unwrap_if_envelope(mmif, runtime_conf)
450
465
  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)
466
+ error_view = mmif.new_view()
467
+ self.sign_view(error_view, runtime_conf)
459
468
  exc_info = sys.exc_info()
460
469
  error_view.set_error(f'{exc_info[0]}: {exc_info[1]}',
461
470
  '\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.1
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.6.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.6.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.1",
3
+ "distance": 0,
4
+ "node": "g7c4f92f3cf112bc39ac00f98b570fa3a77dde470",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-27"
8
+ }
@@ -1,4 +1,5 @@
1
1
  "``clams-python`` version","``mmif-python`` version","Target MMIF Specification"
2
+ `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
3
  `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
4
  `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
5
  `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.6.0",
23
23
  "Flask>=2",
24
24
  "Flask-RESTful>=0.3.9",
25
25
  "gunicorn>=20",
@@ -5,7 +5,9 @@ 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
@@ -334,11 +336,6 @@ class TestClamsApp(unittest.TestCase):
334
336
  self.assertEqual(len(v.metadata.parameters), 1) # 'pretty` parameter was passed
335
337
  out_mmif = Mmif(self.app.annotate(out_mmif))
336
338
  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
339
 
343
340
  def test_run_id(self):
344
341
  # first run
@@ -360,6 +357,31 @@ class TestClamsApp(unittest.TestCase):
360
357
  else:
361
358
  self.assertEqual(second_timestamp, view.metadata.timestamp)
362
359
 
360
+ def test_run_id_reuses_mmif_timestamp(self):
361
+ # the run ID is the timestamp mmif-python assigned to the first view
362
+ # of the run; clams-python does not generate its own
363
+ sentinel = datetime(2001, 2, 3, 4, 5, 6, tzinfo=timezone.utc)
364
+ original_annotate = self.app._annotate
365
+
366
+ def _annotate(mmif, **kwargs):
367
+ out = original_annotate(mmif, **kwargs)
368
+ next(reversed(out.views)).metadata.timestamp = sentinel
369
+ return out
370
+
371
+ # serialize the sentinel the same way mmif-python does
372
+ ref = ExampleInputMMIF.get_rawmmif()
373
+ ref.new_view().metadata.timestamp = sentinel
374
+ expected = next(reversed(
375
+ Mmif(ref.serialize(), validate=False).views)).metadata.timestamp
376
+ with patch.object(self.app, '_annotate', new=_annotate):
377
+ out_mmif = Mmif(self.app.annotate(self.in_mmif))
378
+ app_views = [v for v in out_mmif.views
379
+ if v.metadata.app == str(self.app.metadata.identifier)]
380
+ # one contentful view and one warning view
381
+ self.assertEqual(len(app_views), 2)
382
+ for view in app_views:
383
+ self.assertEqual(expected, view.metadata.timestamp)
384
+
363
385
  def test_annotate_returns_invalid_mmif(self):
364
386
  m = Mmif(self.in_mmif)
365
387
  v = m.new_view()
@@ -430,21 +452,38 @@ class TestClamsApp(unittest.TestCase):
430
452
 
431
453
  def test_error_handling(self):
432
454
  params = {'raise_error': ['true'], 'pretty': ['true']}
433
- in_mmif = Mmif(self.in_mmif)
434
455
  try:
435
- out_mmif = self.app.annotate(in_mmif, **params)
456
+ out_mmif = self.app.annotate(self.in_mmif, **params)
436
457
  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
458
+ out_mmif = self.app.set_error_view(self.in_mmif, **params)
443
459
  self.assertIsNotNone(out_mmif)
460
+ self.assertEqual(len(out_mmif.views), 1)
444
461
  last_view: View = next(reversed(out_mmif.views))
445
462
  self.assertEqual(len(last_view.metadata.contains), 0)
446
463
  self.assertEqual(len(last_view.metadata.error), 2)
447
464
 
465
+ def test_error_view_keeps_previous_run(self):
466
+ # an error in a later run of the same app must not overwrite
467
+ # the views from an earlier, successful run
468
+ first = Mmif(self.app.annotate(self.in_mmif))
469
+ params = {'raise_error': ['true']}
470
+ try:
471
+ self.app.annotate(first.serialize(), **params)
472
+ except Exception:
473
+ out_mmif = self.app.set_error_view(first.serialize(), **params)
474
+ self.assertEqual(len(out_mmif.views), len(first.views) + 1)
475
+ for before, after in zip(first.views, out_mmif.views):
476
+ self.assertEqual(before.id, after.id)
477
+ self.assertFalse(after.has_error())
478
+ self.assertEqual(len(before.annotations), len(after.annotations))
479
+ self.assertEqual(before.metadata.timestamp,
480
+ after.metadata.timestamp)
481
+ error_view: View = next(reversed(out_mmif.views))
482
+ self.assertTrue(error_view.has_error())
483
+ self.assertEqual(len(error_view.annotations), 0)
484
+ self.assertNotIn(error_view.metadata.timestamp,
485
+ [v.metadata.timestamp for v in first.views])
486
+
448
487
  def test_empty_view_becomes_warning_view(self):
449
488
  class EmptyClamsApp(clams.app.ClamsApp):
450
489
  def _appmetadata(self):
@@ -494,6 +533,53 @@ class TestClamsApp(unittest.TestCase):
494
533
  self.assertTrue(view.has_error())
495
534
  self.assertFalse(view.has_warnings())
496
535
 
536
+ def test_self_reported_warning_view_is_not_pruned(self):
537
+ class WarningClamsApp(clams.app.ClamsApp):
538
+ def _appmetadata(self):
539
+ pass
540
+ def _annotate(self, mmif, **kwargs):
541
+ new_view = mmif.new_view()
542
+ self.sign_view(new_view, kwargs)
543
+ new_view.metadata.warnings = ['simulated warning']
544
+ return mmif
545
+ out_mmif = Mmif(WarningClamsApp().annotate(self.in_mmif))
546
+ self.assertEqual(len(out_mmif.views), 1)
547
+ view = next(iter(out_mmif.views))
548
+ self.assertEqual(view.metadata.warnings, ['simulated warning'])
549
+
550
+ def test_document_properties_stay_in_own_view(self):
551
+ # an app whose only output is document properties keeps its own view;
552
+ # the generated `Annotation`s must not go to an earlier app's view
553
+ class DocPropsClamsApp(clams.app.ClamsApp):
554
+ def _appmetadata(self):
555
+ pass
556
+ def _annotate(self, mmif, **kwargs):
557
+ new_view = mmif.new_view()
558
+ self.sign_view(new_view, kwargs)
559
+ mmif['v1'].add_property('duration', 1234)
560
+ mmif['t1'].add_property('author', 'me')
561
+ return mmif
562
+ in_mmif = Mmif(self.in_mmif)
563
+ earlier = in_mmif.new_view()
564
+ earlier.metadata.app = 'http://apps.clams.ai/earlier-app/v1'
565
+ earlier.new_annotation(AnnotationTypes.TimeFrame, start=0, end=10)
566
+ app = DocPropsClamsApp()
567
+ out_mmif = Mmif(app.annotate(in_mmif.serialize()))
568
+ self.assertEqual(
569
+ 0, len(list(out_mmif[earlier.id].get_annotations(AnnotationTypes.Annotation))))
570
+ own_views = [v for v in out_mmif.views
571
+ if v.metadata.app == str(app.metadata.identifier)]
572
+ self.assertEqual(len(own_views), 1)
573
+ self.assertFalse(own_views[0].has_warnings())
574
+ # one `Annotation` per document, even though `serialize()` converts
575
+ # the same pending properties a second time
576
+ anns = list(own_views[0].get_annotations(AnnotationTypes.Annotation))
577
+ self.assertEqual(len(anns), 2)
578
+ props = {a.get_property('document'):
579
+ {k: v for k, v in a.properties.items() if k not in ('id', 'document')}
580
+ for a in anns}
581
+ self.assertEqual({'v1': {'duration': 1234}, 't1': {'author': 'me'}}, props)
582
+
497
583
  def test_gpu_mem_fields_default_zero(self):
498
584
  """GPU memory fields default to 0."""
499
585
  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