mmif-python 1.2.1__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. {mmif_python-1.2.1 → mmif_python-1.3.0}/PKG-INFO +11 -8
  2. {mmif_python-1.2.1 → mmif_python-1.3.0}/README.md +9 -7
  3. mmif_python-1.3.0/VERSION +1 -0
  4. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/__init__.py +8 -8
  5. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/serialize/__init__.py +4 -0
  6. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/serialize/annotation.py +2 -2
  7. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/serialize/mmif.py +8 -8
  8. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/serialize/model.py +4 -1
  9. mmif_python-1.3.0/mmif/utils/__init__.py +4 -0
  10. mmif_python-1.3.0/mmif/utils/cli/__init__.py +226 -0
  11. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/utils/cli/describe.py +56 -52
  12. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/utils/cli/rewind.py +7 -10
  13. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/utils/cli/source.py +4 -7
  14. mmif_python-1.3.0/mmif/utils/cli/summarize.py +65 -0
  15. mmif_python-1.3.0/mmif/utils/summarizer/__init__.py +28 -0
  16. mmif_python-1.3.0/mmif/utils/summarizer/config.py +69 -0
  17. mmif_python-1.3.0/mmif/utils/summarizer/graph.py +256 -0
  18. mmif_python-1.3.0/mmif/utils/summarizer/nodes.py +370 -0
  19. mmif_python-1.3.0/mmif/utils/summarizer/summary.py +657 -0
  20. mmif_python-1.3.0/mmif/utils/summarizer/utils.py +268 -0
  21. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/utils/video_document_helper.py +270 -21
  22. mmif_python-1.3.0/mmif/utils/workflow_helper.py +554 -0
  23. mmif_python-1.3.0/mmif/ver/__init__.py +2 -0
  24. mmif_python-1.3.0/mmif_docloc_http/__init__.py +30 -0
  25. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif_python.egg-info/PKG-INFO +11 -8
  26. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif_python.egg-info/SOURCES.txt +7 -0
  27. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif_python.egg-info/requires.txt +1 -0
  28. {mmif_python-1.2.1 → mmif_python-1.3.0}/requirements.txt +1 -0
  29. {mmif_python-1.2.1 → mmif_python-1.3.0}/setup.py +1 -41
  30. {mmif_python-1.2.1 → mmif_python-1.3.0}/tests/test_serialize.py +34 -0
  31. {mmif_python-1.2.1 → mmif_python-1.3.0}/tests/test_utils.py +300 -21
  32. mmif_python-1.3.0/tests/test_utils_cli.py +368 -0
  33. mmif_python-1.2.1/VERSION +0 -1
  34. mmif_python-1.2.1/mmif/utils/__init__.py +0 -0
  35. mmif_python-1.2.1/mmif/utils/cli/__init__.py +0 -4
  36. mmif_python-1.2.1/mmif/utils/workflow_helper.py +0 -466
  37. mmif_python-1.2.1/mmif/ver/__init__.py +0 -2
  38. mmif_python-1.2.1/mmif_docloc_http/__init__.py +0 -16
  39. mmif_python-1.2.1/tests/test_utils_cli.py +0 -306
  40. {mmif_python-1.2.1 → mmif_python-1.3.0}/LICENSE +0 -0
  41. {mmif_python-1.2.1 → mmif_python-1.3.0}/MANIFEST.in +0 -0
  42. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/res/__init__.py +0 -0
  43. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/res/clams.vocabulary.yaml +0 -0
  44. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/res/do-not-edit.txt +0 -0
  45. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/res/mmif.json +0 -0
  46. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/serialize/view.py +0 -0
  47. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/utils/sequence_helper.py +0 -0
  48. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/utils/text_document_helper.py +0 -0
  49. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/utils/timeunit_helper.py +0 -0
  50. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/ver/do-not-edit.txt +0 -0
  51. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/vocabulary/__init__.py +0 -0
  52. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/vocabulary/annotation_types.py +0 -0
  53. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/vocabulary/base_types.py +0 -0
  54. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/vocabulary/do-not-edit.txt +0 -0
  55. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/vocabulary/document_types.py +0 -0
  56. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif_python.egg-info/dependency_links.txt +0 -0
  57. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif_python.egg-info/entry_points.txt +0 -0
  58. {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif_python.egg-info/top_level.txt +0 -0
  59. {mmif_python-1.2.1 → mmif_python-1.3.0}/requirements.cv +0 -0
  60. {mmif_python-1.2.1 → mmif_python-1.3.0}/requirements.seq +0 -0
  61. {mmif_python-1.2.1 → mmif_python-1.3.0}/setup.cfg +0 -0
  62. {mmif_python-1.2.1 → mmif_python-1.3.0}/tests/test_versioncompat.py +0 -0
  63. {mmif_python-1.2.1 → mmif_python-1.3.0}/tests/test_vocab.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mmif-python
3
- Version: 1.2.1
3
+ Version: 1.3.0
4
4
  Summary: Python implementation of MultiMedia Interchange Format specification. (https://mmif.clams.ai)
5
5
  Home-page: https://mmif.clams.ai
6
6
  Author: Brandeis Lab for Linguistics and Computation
@@ -13,6 +13,7 @@ Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: orderly-set==5.3.*
15
15
  Requires-Dist: jsonschema
16
+ Requires-Dist: pydantic>=2.0
16
17
  Provides-Extra: seq
17
18
  Requires-Dist: numpy; extra == "seq"
18
19
  Provides-Extra: cv
@@ -38,20 +39,22 @@ Dynamic: requires-python
38
39
  Dynamic: summary
39
40
 
40
41
  ## MultiMedia Interchange Format
41
- [MMIF](https://mmif.clams.ai) is a JSON(-LD)-based data format designed for transferring annotation data between computational analysis applications in [CLAMS project](https://clams.ai).
42
+
43
+ [MMIF](https://mmif.clams.ai) is a JSON(-LD)-based data format designed for transferring annotation data between computational analysis applications of the [CLAMS project](https://clams.ai).
42
44
 
43
45
 
44
46
  ## mmif-python
45
- `mmif-python` is a Python implementation of the MMIF data format.
46
- `mmif-python` provides various helper classes and functions to handle MMIF JSON in Python,
47
- including ;
48
47
 
49
- 1. de-/serialization of MMIF internal data structures to/from JSON
48
+ `mmif-python` is a Python implementation of the MMIF data format. It provides various helper classes and functions to handle MMIF JSON in Python, including:
49
+
50
+ 1. serialization and de-serialization of MMIF internal data structures to/from JSON
50
51
  2. validation of MMIF JSON
51
52
  3. handling of CLAMS vocabulary types
52
- 4. navigation of MMIF object via various "search" methods (e.g. `mmif.get_all_views_contain(vocab_type))`)
53
+ 4. navigation of MMIF objects via various "search" methods (e.g. `mmif.get_all_views_contain(vocab_type)`)
53
54
 
54
55
  ## For more ...
56
+
55
57
  * [Version history and patch notes](https://github.com/clamsproject/mmif-python/blob/main/CHANGELOG.md)
56
- * [MMIF Python API documentation](https://clamsproject.github.io/mmif-python)
58
+ * [MMIF Python API documentation](https://clamsproject.github.io/mmif-python/latest)
57
59
  * [MMIF JSON specification and schema](https://clamsproject.github.io/mmif)
60
+ * [Contributing guide](CONTRIBUTING.md)
@@ -1,18 +1,20 @@
1
1
  ## MultiMedia Interchange Format
2
- [MMIF](https://mmif.clams.ai) is a JSON(-LD)-based data format designed for transferring annotation data between computational analysis applications in [CLAMS project](https://clams.ai).
2
+
3
+ [MMIF](https://mmif.clams.ai) is a JSON(-LD)-based data format designed for transferring annotation data between computational analysis applications of the [CLAMS project](https://clams.ai).
3
4
 
4
5
 
5
6
  ## mmif-python
6
- `mmif-python` is a Python implementation of the MMIF data format.
7
- `mmif-python` provides various helper classes and functions to handle MMIF JSON in Python,
8
- including ;
9
7
 
10
- 1. de-/serialization of MMIF internal data structures to/from JSON
8
+ `mmif-python` is a Python implementation of the MMIF data format. It provides various helper classes and functions to handle MMIF JSON in Python, including:
9
+
10
+ 1. serialization and de-serialization of MMIF internal data structures to/from JSON
11
11
  2. validation of MMIF JSON
12
12
  3. handling of CLAMS vocabulary types
13
- 4. navigation of MMIF object via various "search" methods (e.g. `mmif.get_all_views_contain(vocab_type))`)
13
+ 4. navigation of MMIF objects via various "search" methods (e.g. `mmif.get_all_views_contain(vocab_type)`)
14
14
 
15
15
  ## For more ...
16
+
16
17
  * [Version history and patch notes](https://github.com/clamsproject/mmif-python/blob/main/CHANGELOG.md)
17
- * [MMIF Python API documentation](https://clamsproject.github.io/mmif-python)
18
+ * [MMIF Python API documentation](https://clamsproject.github.io/mmif-python/latest)
18
19
  * [MMIF JSON specification and schema](https://clamsproject.github.io/mmif)
20
+ * [Contributing guide](CONTRIBUTING.md)
@@ -0,0 +1 @@
1
+ 1.3.0
@@ -34,28 +34,28 @@ def find_all_modules(pkgname):
34
34
 
35
35
 
36
36
  def prep_argparser_and_subcmds():
37
- parser = argparse.ArgumentParser()
37
+ parser = argparse.ArgumentParser(prog='mmif')
38
38
  parser.add_argument(
39
39
  '-v', '--version',
40
40
  action='version',
41
41
  version=version_template.format(__version__, __specver__)
42
42
  )
43
43
  subparsers = parser.add_subparsers(title='sub-command', dest='subcmd')
44
- return parser, subparsers
45
-
46
-
47
- def cli():
48
- parser, subparsers = prep_argparser_and_subcmds()
49
- cli_modules = {}
44
+ subcmds = {}
50
45
  for cli_module in find_all_modules('mmif.utils.cli'):
51
46
  cli_module_name = cli_module.__name__.rsplit('.')[-1]
52
- cli_modules[cli_module_name] = cli_module
47
+ subcmds[cli_module_name] = cli_module
53
48
  subcmd_parser = cli_module.prep_argparser(add_help=False)
54
49
  subparsers.add_parser(cli_module_name, parents=[subcmd_parser],
55
50
  help=cli_module.describe_argparser()[0],
56
51
  description=cli_module.describe_argparser()[1],
57
52
  formatter_class=argparse.RawDescriptionHelpFormatter,
58
53
  )
54
+ return parser, subparsers, subcmds
55
+
56
+
57
+ def cli():
58
+ parser, subparsers, cli_modules = prep_argparser_and_subcmds()
59
59
  if len(sys.argv) == 1:
60
60
  parser.print_help(sys.stderr)
61
61
  sys.exit(1)
@@ -1,3 +1,7 @@
1
+ """
2
+ Core package to provide serialization and deserialization of MMIF format.
3
+ """
4
+
1
5
  from .annotation import *
2
6
  from .annotation import __all__ as anno_all
3
7
  from .mmif import *
@@ -374,7 +374,7 @@ class Document(Annotation):
374
374
 
375
375
  With the former method, the SDK will record the added property as a
376
376
  `Annotation` annotation object, separate from the original `Document`
377
- object. See :meth:`.Mmif.generate_capital_annotations()` for more.
377
+ object. See :meth:`mmif.serialize.mmif.Mmif.generate_capital_annotations` for more.
378
378
 
379
379
  A few notes to keep in mind:
380
380
 
@@ -442,7 +442,7 @@ class Document(Annotation):
442
442
  See Also
443
443
  --------
444
444
  add_property : Add a new property to the document
445
- Mmif.generate_capital_annotations : How pending properties are serialized
445
+ mmif.serialize.mmif.Mmif.generate_capital_annotations : How pending properties are serialized
446
446
  """
447
447
  if prop_name == 'id':
448
448
  # because all three dicts have `id` key as required field, we need
@@ -14,8 +14,8 @@ import json
14
14
  import math
15
15
  import warnings
16
16
  from collections import defaultdict
17
- from datetime import datetime
18
- from typing import List, Union, Optional, Dict, cast, Iterator
17
+ from datetime import datetime, timezone
18
+ from typing import Any, List, Union, Optional, Dict, cast, Iterator
19
19
 
20
20
  import jsonschema.validators
21
21
 
@@ -24,7 +24,7 @@ from mmif import ThingTypesBase
24
24
  from mmif.serialize.annotation import Annotation, Document
25
25
  from mmif.serialize.model import MmifObject, DataList
26
26
  from mmif.serialize.view import View
27
- from mmif.vocabulary import AnnotationTypes, DocumentTypes
27
+ from mmif.vocabulary import AnnotationTypes, DocumentTypesBase
28
28
 
29
29
  __all__ = ['Mmif']
30
30
 
@@ -433,7 +433,7 @@ class Mmif(MmifObject):
433
433
  """
434
434
  new_view = View()
435
435
  new_view.id = self.new_view_id()
436
- new_view.metadata.timestamp = datetime.now()
436
+ new_view.metadata.timestamp = datetime.now(timezone.utc)
437
437
  self.add_view(new_view)
438
438
  return new_view
439
439
 
@@ -487,11 +487,11 @@ class Mmif(MmifObject):
487
487
  else:
488
488
  return []
489
489
 
490
- def get_documents_by_type(self, doc_type: Union[str, DocumentTypes]) -> List[Document]:
490
+ def get_documents_by_type(self, doc_type: DocumentTypesBase) -> List[Document]:
491
491
  """
492
492
  Method to get all documents where the type matches a particular document type, which should be one of the CLAMS document types.
493
493
 
494
- :param doc_type: the type of documents to search for, must be one of ``Document`` type defined in the CLAMS vocabulary.
494
+ :param doc_type: the type of documents to search for, must be one of ``Document`` types defined in the CLAMS vocabulary.
495
495
  :return: a list of documents matching the requested type, or an empty list if none found.
496
496
  """
497
497
  docs = []
@@ -530,7 +530,7 @@ class Mmif(MmifObject):
530
530
  docs.extend([document for document in self.documents if document[prop_key] == prop_value])
531
531
  return docs
532
532
 
533
- def get_documents_locations(self, m_type: Union[DocumentTypes, str], path_only=False) -> List[Union[str, None]]:
533
+ def get_documents_locations(self, m_type: Union[DocumentTypesBase, str], path_only=False) -> List[Union[str, None]]:
534
534
  """
535
535
  This method returns the file paths of documents of given type.
536
536
  Only top-level documents have locations, so we only check them.
@@ -545,7 +545,7 @@ class Mmif(MmifObject):
545
545
  else:
546
546
  return [doc.location for doc in docs]
547
547
 
548
- def get_document_location(self, m_type: Union[DocumentTypes, str], path_only=False) -> Optional[str]:
548
+ def get_document_location(self, m_type: Union[DocumentTypesBase, str], path_only=False) -> Optional[str]:
549
549
  """
550
550
  Method to get the location of *first* document of given type.
551
551
 
@@ -402,7 +402,10 @@ class MmifObjectEncoder(json.JSONEncoder):
402
402
  if hasattr(obj, '_serialize'):
403
403
  return obj._serialize()
404
404
  elif hasattr(obj, 'isoformat'): # for datetime objects
405
- return obj.isoformat()
405
+ s = obj.isoformat()
406
+ if s.endswith('+00:00'):
407
+ s = s[:-6] + 'Z'
408
+ return s
406
409
  elif hasattr(obj, '__str__'):
407
410
  return str(obj)
408
411
  else:
@@ -0,0 +1,4 @@
1
+ """
2
+ Package containing utility modules for handling different types of source
3
+ documents, and general implementation of common data structures and algorithms.
4
+ """
@@ -0,0 +1,226 @@
1
+ """
2
+ Package containing CLI modules.
3
+ """
4
+
5
+ import contextlib
6
+ import io
7
+ import os
8
+ import sys
9
+ from typing import Iterator, Optional, TextIO, Type, Union, cast, get_args, get_origin
10
+
11
+ from pydantic import BaseModel
12
+
13
+
14
+ @contextlib.contextmanager
15
+ def open_cli_io_arg(
16
+ path_or_dash: Optional[str],
17
+ mode: str = "r",
18
+ encoding: Optional[str] = None,
19
+ errors: Optional[str] = None,
20
+ default_stdin: bool = False,
21
+ ) -> Iterator[TextIO]:
22
+ """
23
+ Context manager for opening files with stdin/stdout support.
24
+
25
+ This function is intended for plain text streams (e.g. JSON/MMIF) and does
26
+ not support binary modes (e.g., 'rb', 'wb').
27
+
28
+ This is a native replacement for argparse.FileType which is deprecated as
29
+ of Python 3.14 due to resource leak issues. Unlike FileType, this defers
30
+ file opening until actually needed and ensures proper cleanup via context
31
+ manager.
32
+
33
+ Handles the common CLI pattern where:
34
+
35
+ - '-' means stdin (read mode) or stdout (write mode)
36
+ - None means "argument not provided"; when default_stdin=True, it falls back
37
+ to stdin/stdout
38
+ - Regular paths open actual files with proper resource management
39
+
40
+ :param path_or_dash: File path, '-' for stdin/stdout, or None for no argument
41
+ :param mode: File mode ('r' for reading, 'w' for writing). Binary modes are
42
+ not supported.
43
+ :param encoding: Optional file encoding
44
+ :param errors: Optional error handling strategy for encoding
45
+ :param default_stdin: If True and path_or_dash is None, default to stdin
46
+ (mode 'r') or stdout (mode 'w')
47
+ :returns: Context manager yielding text-mode file handle
48
+ :rtype: Iterator[TextIO]
49
+
50
+ Example usage::
51
+
52
+ # Read from file or stdin
53
+ with open_cli_io_arg(args.input, 'r', default_stdin=True) as f:
54
+ content = f.read()
55
+
56
+ # Write to file or stdout
57
+ with open_cli_io_arg(args.output, 'w', default_stdin=True) as f:
58
+ f.write(content)
59
+ """
60
+ # Valid text modes for file operations
61
+ _READ_FLAGS = frozenset({"r", "+"})
62
+ _WRITE_FLAGS = frozenset({"w", "a", "x", "+"})
63
+
64
+ if "b" in mode:
65
+ raise ValueError(
66
+ f"Binary mode '{mode}' is not supported. "
67
+ "Use text modes ('r', 'w', 'a', 'x') instead."
68
+ )
69
+
70
+ needs_read = bool(set(mode) & _READ_FLAGS)
71
+ needs_write = bool(set(mode) & _WRITE_FLAGS)
72
+
73
+ should_use_stdio = path_or_dash == "-" or (path_or_dash is None and default_stdin)
74
+
75
+ file_handle: Optional[TextIO] = None
76
+ should_close = False
77
+
78
+ try:
79
+ if should_use_stdio:
80
+ if needs_read and needs_write:
81
+ raise ValueError(
82
+ f"Mode '{mode}' not supported with stdin/stdout "
83
+ "(use read or write only)"
84
+ )
85
+
86
+ if needs_read:
87
+ # Check for missing input when stdin is a terminal
88
+ if path_or_dash is None and default_stdin and sys.stdin.isatty():
89
+ raise SystemExit("error: No input provided.")
90
+ file_handle = sys.stdin
91
+
92
+ elif needs_write:
93
+ file_handle = sys.stdout
94
+
95
+ else:
96
+ raise ValueError(
97
+ f"Mode '{mode}' not supported with stdin/stdout (use 'r' or 'w')"
98
+ )
99
+
100
+ elif isinstance(path_or_dash, str):
101
+ if needs_read and not os.path.exists(path_or_dash):
102
+ raise FileNotFoundError(f"Input path does not exist: {path_or_dash}")
103
+ file_handle = cast(
104
+ TextIO, io.open(path_or_dash, mode, encoding=encoding, errors=errors)
105
+ )
106
+ should_close = True
107
+
108
+ elif path_or_dash is None:
109
+ # None without default_stdin means no file specified
110
+ raise ValueError(
111
+ "No file path provided. Use '-' for stdin/stdout or set default_stdin=True."
112
+ )
113
+ else:
114
+ raise TypeError(
115
+ f"Invalid type for path_or_dash: {type(path_or_dash).__name__}. "
116
+ "Expected str or None."
117
+ )
118
+
119
+ if file_handle is not None:
120
+ yield file_handle
121
+
122
+ finally:
123
+ if should_close and file_handle is not None:
124
+ file_handle.close()
125
+
126
+
127
+ def generate_model_summary(model: Type[BaseModel], indent: int = 0) -> str:
128
+ lines = []
129
+ prefix = " " * indent
130
+
131
+ # model_fields is a dictionary of FieldInfo objects
132
+ for name, field in model.model_fields.items():
133
+ # Get the alias if available, otherwise use the field name
134
+ field_name = field.alias if field.alias else name
135
+
136
+ # Get type annotation
137
+ type_annotation = field.annotation
138
+
139
+ def format_type(t) -> str:
140
+ origin = get_origin(t)
141
+ args = get_args(t)
142
+
143
+ # Handle Optional (Union[T, None])
144
+ if origin is Union and type(None) in args:
145
+ non_none_args = [arg for arg in args if arg is not type(None)]
146
+ if len(non_none_args) == 1:
147
+ return f"{format_type(non_none_args[0])}, optional"
148
+
149
+ # Handle List
150
+ if origin is list:
151
+ if args:
152
+ return f"[{format_type(args[0])}]"
153
+ return "[]"
154
+
155
+ # Handle Dict
156
+ if origin is dict:
157
+ return "obj"
158
+
159
+ # Handle Pydantic Models (Custom Classes)
160
+ if isinstance(t, type) and issubclass(t, BaseModel):
161
+ return "obj"
162
+
163
+ # Handle basic types and cleanup
164
+ t_str = str(t)
165
+ if t_str.startswith("<class '"):
166
+ t_str = t_str[8:-2]
167
+ if t_str.startswith("typing."):
168
+ t_str = t_str[7:]
169
+
170
+ # Remove module prefix if present
171
+ if "." in t_str:
172
+ t_str = t_str.split(".")[-1]
173
+
174
+ return t_str
175
+
176
+ display_type = format_type(type_annotation)
177
+
178
+ description = field.description if field.description else ""
179
+
180
+ line_content = f"{prefix}- {field_name} ({display_type})"
181
+ if description:
182
+ line_content += f": {description}"
183
+ lines.append(line_content)
184
+
185
+ # Check if it's a Pydantic model or a list/dict of Pydantic models
186
+ origin = get_origin(type_annotation)
187
+ args = get_args(type_annotation)
188
+
189
+ nested_model = None
190
+ # Handle Optional wrappers for nesting check
191
+ check_type = type_annotation
192
+ if origin is Union and type(None) in args:
193
+ non_none_args = [arg for arg in args if arg is not type(None)]
194
+ if len(non_none_args) == 1:
195
+ check_type = non_none_args[0]
196
+ origin = get_origin(check_type)
197
+ args = get_args(check_type)
198
+
199
+ if isinstance(check_type, type) and issubclass(check_type, BaseModel):
200
+ nested_model = check_type
201
+ elif (
202
+ origin is list
203
+ and args
204
+ and isinstance(args[0], type)
205
+ and issubclass(args[0], BaseModel)
206
+ ):
207
+ nested_model = args[0]
208
+ elif (
209
+ origin is dict
210
+ and args
211
+ and len(args) > 1
212
+ and isinstance(args[1], type)
213
+ and issubclass(args[1], BaseModel)
214
+ ):
215
+ nested_model = args[1]
216
+
217
+ if nested_model:
218
+ lines.append(generate_model_summary(nested_model, indent + 4))
219
+
220
+ return "\n".join(lines)
221
+
222
+
223
+ # keep imports of CLI modules for historical reasons
224
+ # keep them here in the bottom to avoid circular imports
225
+ from mmif.utils.cli import rewind
226
+ from mmif.utils.cli import source
@@ -3,12 +3,18 @@ import json
3
3
  import sys
4
4
  import textwrap
5
5
  from pathlib import Path
6
- from typing import Union
6
+ from typing import Union, cast
7
+
8
+ from mmif.utils.cli import open_cli_io_arg, generate_model_summary
7
9
 
8
- from mmif.utils.workflow_helper import generate_workflow_identifier, describe_single_mmif, \
9
- describe_mmif_collection
10
10
  # gen_param_hash is imported for backward compatibility
11
- from mmif.utils.workflow_helper import generate_param_hash
11
+ from mmif.utils.workflow_helper import (
12
+ CollectionMmifDesc,
13
+ SingleMmifDesc,
14
+ describe_mmif_collection,
15
+ describe_single_mmif,
16
+ generate_workflow_identifier,
17
+ )
12
18
 
13
19
 
14
20
  def get_pipeline_specs(mmif_file: Union[str, Path]):
@@ -22,41 +28,30 @@ def generate_pipeline_identifier(mmif_file: Union[str, Path]) -> str:
22
28
  import warnings
23
29
  warnings.warn("generate_pipeline_identifier is deprecated, use generate_workflow_identifier instead",
24
30
  DeprecationWarning)
25
- return generate_workflow_identifier(mmif_file)
31
+ return cast(str, generate_workflow_identifier(mmif_file))
26
32
 
27
33
 
28
34
  def describe_argparser():
29
- """
30
- Returns two strings: one-line description of the argparser, and
31
- additional material, which will be shown in `clams --help` and
32
- `clams <subcmd> --help`, respectively.
33
- """
34
35
  oneliner = (
35
- 'provides CLI to describe the workflow specification from a MMIF '
36
- 'file or a collection of MMIF files.'
36
+ 'Describe the workflow specification from a MMIF file or a '
37
+ 'collection of MMIF files.'
37
38
  )
38
39
 
39
- # get and clean docstrings
40
- single_doc = describe_single_mmif.__doc__.split(':param')[0]
41
- single_doc = textwrap.dedent(single_doc).strip()
42
- collection_doc = describe_mmif_collection.__doc__.split(':param')[0]
43
- collection_doc = textwrap.dedent(collection_doc).strip()
44
-
45
40
  additional = textwrap.dedent(f"""
46
41
  This command extracts workflow information from a single MMIF file or
47
- summarizes a directory of MMIF files.
42
+ a directory of MMIF files. The output is serialized as JSON.
43
+
44
+ Output Schemas:
45
+
46
+ 1. Single MMIF File (mmif-file):
47
+ {generate_model_summary(SingleMmifDesc, indent=4)}
48
+
49
+ 2. MMIF Collection (mmif-dir):
50
+ {generate_model_summary(CollectionMmifDesc, indent=4)}
48
51
 
49
- ==========================
50
- For a single MMIF file
51
- ==========================
52
- {single_doc}
53
-
54
- ===============================
55
- For a directory of MMIF files
56
- ===============================
57
- {collection_doc}
52
+ Use `--help-schema` to inspect the full JSON schema for a specific output type.
58
53
  """)
59
- return oneliner, oneliner + '\n\n' + additional.strip()
54
+ return oneliner, additional
60
55
 
61
56
 
62
57
  def prep_argparser(**kwargs):
@@ -65,17 +60,17 @@ def prep_argparser(**kwargs):
65
60
  formatter_class=argparse.RawDescriptionHelpFormatter,
66
61
  **kwargs
67
62
  )
63
+
68
64
  parser.add_argument(
69
65
  "MMIF_FILE",
70
66
  nargs="?",
71
67
  type=str,
72
- default=None if sys.stdin.isatty() else sys.stdin,
68
+ default=None,
73
69
  help='input MMIF file, a directory of MMIF files, or STDIN if `-` or not provided.'
74
70
  )
75
71
  parser.add_argument(
76
72
  "-o", "--output",
77
- type=argparse.FileType("w"),
78
- default=sys.stdout,
73
+ type=str, default=None,
79
74
  help='output file path, or STDOUT if not provided.'
80
75
  )
81
76
  parser.add_argument(
@@ -83,33 +78,43 @@ def prep_argparser(**kwargs):
83
78
  action="store_true",
84
79
  help="Pretty-print JSON output"
85
80
  )
81
+ parser.add_argument(
82
+ "--help-schema",
83
+ nargs=1,
84
+ choices=["mmif-file", "mmif-dir"],
85
+ metavar="SCHEMA_NAME",
86
+ help="Print the JSON schema for the output. Options: mmif-file, mmif-dir."
87
+ )
86
88
  return parser
87
89
 
88
90
 
89
91
  def main(args):
90
92
  """
91
- Main entry point for the describe CLI command.
92
-
93
- Reads a MMIF file and outputs a JSON summary containing:
94
- - workflow_id: unique identifier for the source and app sequence
95
- - stats: view counts, annotation counts (total/per-view/per-type),
96
- and lists of error/warning/empty view IDs
97
- - views: map of view IDs to app configurations and profiling data
98
-
99
- :param args: Parsed command-line arguments
93
+ Main block for the describe CLI command.
94
+ This function basically works as a wrapper around
95
+ :func:`describe_single_mmif` (for single file input) or
96
+ :func:`describe_mmif_collection` (for directory input).
100
97
  """
98
+ if hasattr(args, 'help_schema') and args.help_schema is not None:
99
+ schema_name = args.help_schema[0]
100
+ if schema_name == 'mmif-file':
101
+ model_cls = SingleMmifDesc
102
+ elif schema_name == 'mmif-dir':
103
+ model_cls = CollectionMmifDesc
104
+
105
+ schema = model_cls.model_json_schema()
106
+ print(json.dumps(schema, indent=2))
107
+ sys.exit(0)
108
+
101
109
  output = {}
102
110
  # if input is a directory
103
- if isinstance(args.MMIF_FILE, str) and Path(args.MMIF_FILE).is_dir():
111
+ if Path(str(args.MMIF_FILE)).is_dir():
104
112
  output = describe_mmif_collection(args.MMIF_FILE)
105
113
  # if input is a file or stdin
106
114
  else:
107
115
  # Read MMIF content
108
- if hasattr(args.MMIF_FILE, 'read'):
109
- mmif_content = args.MMIF_FILE.read()
110
- else:
111
- with open(args.MMIF_FILE, 'r') as f:
112
- mmif_content = f.read()
116
+ with open_cli_io_arg(args.MMIF_FILE, 'r', default_stdin=True) as input_file:
117
+ mmif_content = input_file.read()
113
118
 
114
119
  # For file input, we need to handle the path
115
120
  # If input is from stdin, create a temp file
@@ -127,11 +132,10 @@ def main(args):
127
132
  tmp_path.unlink()
128
133
 
129
134
  if output:
130
- if args.pretty:
131
- json.dump(output, args.output, indent=2)
132
- else:
133
- json.dump(output, args.output)
134
- args.output.write('\n')
135
+ # Convert Pydantic models to dicts
136
+ with open_cli_io_arg(args.output, 'w', default_stdin=True) as output_file:
137
+ json.dump(output, output_file, indent=2 if args.pretty else None)
138
+ output_file.write('\n')
135
139
 
136
140
 
137
141
  if __name__ == "__main__":
@@ -3,6 +3,7 @@ import sys
3
3
  import textwrap
4
4
 
5
5
  import mmif
6
+ from mmif.utils.cli import open_cli_io_arg
6
7
  from mmif.utils.workflow_helper import group_views_by_app
7
8
 
8
9
 
@@ -55,10 +56,6 @@ def rewind_mmif(mmif_obj: mmif.Mmif, choice: int, choice_is_viewnum: bool = True
55
56
 
56
57
 
57
58
  def describe_argparser():
58
- """
59
- returns two strings: one-line description of the argparser, and addition material,
60
- which will be shown in `clams --help` and `clams <subcmd> --help`, respectively.
61
- """
62
59
  oneliner = 'provides CLI to rewind a MMIF from a CLAMS workflow.'
63
60
  additional = textwrap.dedent("""
64
61
  MMIF rewinder rewinds a MMIF by deleting the last N views.
@@ -70,12 +67,10 @@ def prep_argparser(**kwargs):
70
67
  parser = argparse.ArgumentParser(description=describe_argparser()[1],
71
68
  formatter_class=argparse.RawDescriptionHelpFormatter, **kwargs)
72
69
  parser.add_argument("MMIF_FILE",
73
- nargs="?", type=argparse.FileType("r"),
74
- default=None if sys.stdin.isatty() else sys.stdin,
70
+ nargs="?", type=str, default=None,
75
71
  help='input MMIF file path, or STDIN if `-` or not provided.')
76
72
  parser.add_argument("-o", "--output",
77
- type=argparse.FileType("w"),
78
- default=sys.stdout,
73
+ type=str, default=None,
79
74
  help='output file path, or STDOUT if not provided.')
80
75
  parser.add_argument("-p", '--pretty', action='store_true',
81
76
  help="Pretty-print rewound MMIF")
@@ -88,7 +83,8 @@ def prep_argparser(**kwargs):
88
83
 
89
84
 
90
85
  def main(args):
91
- mmif_obj = mmif.Mmif(args.MMIF_FILE.read())
86
+ with open_cli_io_arg(args.MMIF_FILE, 'r', default_stdin=True) as input_file:
87
+ mmif_obj = mmif.Mmif(input_file.read())
92
88
 
93
89
  if args.number == 0: # If user doesn't know how many views to rewind, give them choices.
94
90
  choice = prompt_user(mmif_obj)
@@ -97,7 +93,8 @@ def main(args):
97
93
  if not isinstance(choice, int) or choice <= 0:
98
94
  raise ValueError(f"Only can rewind by a positive number of views. Got {choice}.")
99
95
 
100
- args.output.write(rewind_mmif(mmif_obj, choice, args.mode == 'view').serialize(pretty=args.pretty))
96
+ with open_cli_io_arg(args.output, 'w', default_stdin=True) as output_file:
97
+ output_file.write(rewind_mmif(mmif_obj, choice, args.mode == 'view').serialize(pretty=args.pretty))
101
98
 
102
99
 
103
100
  if __name__ == "__main__":