mmif-python 1.2.0__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 (64) hide show
  1. {mmif_python-1.2.0 → mmif_python-1.3.0}/PKG-INFO +11 -8
  2. {mmif_python-1.2.0 → mmif_python-1.3.0}/README.md +9 -7
  3. mmif_python-1.3.0/VERSION +1 -0
  4. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/__init__.py +8 -8
  5. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/serialize/__init__.py +4 -0
  6. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/serialize/annotation.py +3 -3
  7. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/serialize/mmif.py +9 -9
  8. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/serialize/model.py +4 -1
  9. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/serialize/view.py +1 -1
  10. mmif_python-1.3.0/mmif/utils/__init__.py +4 -0
  11. mmif_python-1.3.0/mmif/utils/cli/__init__.py +226 -0
  12. mmif_python-1.3.0/mmif/utils/cli/describe.py +144 -0
  13. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/utils/cli/rewind.py +22 -32
  14. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/utils/cli/source.py +5 -8
  15. mmif_python-1.3.0/mmif/utils/cli/summarize.py +65 -0
  16. mmif_python-1.3.0/mmif/utils/summarizer/__init__.py +28 -0
  17. mmif_python-1.3.0/mmif/utils/summarizer/config.py +69 -0
  18. mmif_python-1.3.0/mmif/utils/summarizer/graph.py +256 -0
  19. mmif_python-1.3.0/mmif/utils/summarizer/nodes.py +370 -0
  20. mmif_python-1.3.0/mmif/utils/summarizer/summary.py +657 -0
  21. mmif_python-1.3.0/mmif/utils/summarizer/utils.py +268 -0
  22. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/utils/video_document_helper.py +270 -21
  23. mmif_python-1.3.0/mmif/utils/workflow_helper.py +554 -0
  24. mmif_python-1.3.0/mmif/ver/__init__.py +2 -0
  25. mmif_python-1.3.0/mmif_docloc_http/__init__.py +30 -0
  26. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif_python.egg-info/PKG-INFO +11 -8
  27. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif_python.egg-info/SOURCES.txt +8 -0
  28. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif_python.egg-info/requires.txt +1 -0
  29. {mmif_python-1.2.0 → mmif_python-1.3.0}/requirements.txt +1 -0
  30. {mmif_python-1.2.0 → mmif_python-1.3.0}/setup.py +1 -41
  31. {mmif_python-1.2.0 → mmif_python-1.3.0}/tests/test_serialize.py +34 -0
  32. mmif_python-1.3.0/tests/test_utils.py +566 -0
  33. mmif_python-1.3.0/tests/test_utils_cli.py +368 -0
  34. mmif_python-1.2.0/VERSION +0 -1
  35. mmif_python-1.2.0/mmif/utils/__init__.py +0 -0
  36. mmif_python-1.2.0/mmif/utils/cli/__init__.py +0 -4
  37. mmif_python-1.2.0/mmif/utils/cli/describe.py +0 -333
  38. mmif_python-1.2.0/mmif/ver/__init__.py +0 -2
  39. mmif_python-1.2.0/mmif_docloc_http/__init__.py +0 -16
  40. mmif_python-1.2.0/tests/test_utils.py +0 -224
  41. mmif_python-1.2.0/tests/test_utils_cli.py +0 -170
  42. {mmif_python-1.2.0 → mmif_python-1.3.0}/LICENSE +0 -0
  43. {mmif_python-1.2.0 → mmif_python-1.3.0}/MANIFEST.in +0 -0
  44. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/res/__init__.py +0 -0
  45. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/res/clams.vocabulary.yaml +0 -0
  46. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/res/do-not-edit.txt +0 -0
  47. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/res/mmif.json +0 -0
  48. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/utils/sequence_helper.py +0 -0
  49. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/utils/text_document_helper.py +0 -0
  50. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/utils/timeunit_helper.py +0 -0
  51. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/ver/do-not-edit.txt +0 -0
  52. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/vocabulary/__init__.py +0 -0
  53. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/vocabulary/annotation_types.py +0 -0
  54. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/vocabulary/base_types.py +0 -0
  55. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/vocabulary/do-not-edit.txt +0 -0
  56. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/vocabulary/document_types.py +0 -0
  57. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif_python.egg-info/dependency_links.txt +0 -0
  58. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif_python.egg-info/entry_points.txt +0 -0
  59. {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif_python.egg-info/top_level.txt +0 -0
  60. {mmif_python-1.2.0 → mmif_python-1.3.0}/requirements.cv +0 -0
  61. {mmif_python-1.2.0 → mmif_python-1.3.0}/requirements.seq +0 -0
  62. {mmif_python-1.2.0 → mmif_python-1.3.0}/setup.cfg +0 -0
  63. {mmif_python-1.2.0 → mmif_python-1.3.0}/tests/test_versioncompat.py +0 -0
  64. {mmif_python-1.2.0 → 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.0
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 *
@@ -2,7 +2,7 @@
2
2
  The :mod:`annotation` module contains the classes used to represent a
3
3
  MMIF annotation as a live Python object.
4
4
 
5
- In MMIF, annotations are created by apps in a pipeline as a part
5
+ In MMIF, annotations are created by apps in a workflow as a part
6
6
  of a view. For documentation on how views are represented, see
7
7
  :mod:`mmif.serialize.view`.
8
8
  """
@@ -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
 
@@ -635,7 +635,7 @@ class Mmif(MmifObject):
635
635
  def get_views_for_document(self, doc_id: str) -> List[View]:
636
636
  """
637
637
  Returns the list of all views that have annotations anchored on a particular document.
638
- Note that when the document is inside a view (generated during the pipeline's running),
638
+ Note that when the document is inside a view (generated during the workflow's running),
639
639
  doc_id must be prefixed with the view_id.
640
640
  """
641
641
  views = []
@@ -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:
@@ -2,7 +2,7 @@
2
2
  The :mod:`view` module contains the classes used to represent a MMIF view
3
3
  as a live Python object.
4
4
 
5
- In MMIF, views are created by apps in a pipeline that are annotating
5
+ In MMIF, views are created by apps in a workflow that are annotating
6
6
  data that was previously present in the MMIF file.
7
7
 
8
8
  The :class:`View` class is a high-level container that provides convenient
@@ -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
@@ -0,0 +1,144 @@
1
+ import argparse
2
+ import json
3
+ import sys
4
+ import textwrap
5
+ from pathlib import Path
6
+ from typing import Union, cast
7
+
8
+ from mmif.utils.cli import open_cli_io_arg, generate_model_summary
9
+
10
+ # gen_param_hash is imported for backward compatibility
11
+ from mmif.utils.workflow_helper import (
12
+ CollectionMmifDesc,
13
+ SingleMmifDesc,
14
+ describe_mmif_collection,
15
+ describe_single_mmif,
16
+ generate_workflow_identifier,
17
+ )
18
+
19
+
20
+ def get_pipeline_specs(mmif_file: Union[str, Path]):
21
+ import warnings
22
+ warnings.warn("get_pipeline_specs is deprecated, use mmif.utils.workflow_helper.describe_single_mmif instead",
23
+ DeprecationWarning)
24
+ return describe_single_mmif(mmif_file)
25
+
26
+
27
+ def generate_pipeline_identifier(mmif_file: Union[str, Path]) -> str:
28
+ import warnings
29
+ warnings.warn("generate_pipeline_identifier is deprecated, use generate_workflow_identifier instead",
30
+ DeprecationWarning)
31
+ return cast(str, generate_workflow_identifier(mmif_file))
32
+
33
+
34
+ def describe_argparser():
35
+ oneliner = (
36
+ 'Describe the workflow specification from a MMIF file or a '
37
+ 'collection of MMIF files.'
38
+ )
39
+
40
+ additional = textwrap.dedent(f"""
41
+ This command extracts workflow information from a single MMIF file or
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)}
51
+
52
+ Use `--help-schema` to inspect the full JSON schema for a specific output type.
53
+ """)
54
+ return oneliner, additional
55
+
56
+
57
+ def prep_argparser(**kwargs):
58
+ parser = argparse.ArgumentParser(
59
+ description=describe_argparser()[1],
60
+ formatter_class=argparse.RawDescriptionHelpFormatter,
61
+ **kwargs
62
+ )
63
+
64
+ parser.add_argument(
65
+ "MMIF_FILE",
66
+ nargs="?",
67
+ type=str,
68
+ default=None,
69
+ help='input MMIF file, a directory of MMIF files, or STDIN if `-` or not provided.'
70
+ )
71
+ parser.add_argument(
72
+ "-o", "--output",
73
+ type=str, default=None,
74
+ help='output file path, or STDOUT if not provided.'
75
+ )
76
+ parser.add_argument(
77
+ "-p", "--pretty",
78
+ action="store_true",
79
+ help="Pretty-print JSON output"
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
+ )
88
+ return parser
89
+
90
+
91
+ def main(args):
92
+ """
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).
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
+
109
+ output = {}
110
+ # if input is a directory
111
+ if Path(str(args.MMIF_FILE)).is_dir():
112
+ output = describe_mmif_collection(args.MMIF_FILE)
113
+ # if input is a file or stdin
114
+ else:
115
+ # Read MMIF content
116
+ with open_cli_io_arg(args.MMIF_FILE, 'r', default_stdin=True) as input_file:
117
+ mmif_content = input_file.read()
118
+
119
+ # For file input, we need to handle the path
120
+ # If input is from stdin, create a temp file
121
+ import tempfile
122
+ tmp_path = None
123
+ try:
124
+ with tempfile.NamedTemporaryFile(
125
+ mode='w', suffix='.mmif', delete=False
126
+ ) as tmp:
127
+ tmp.write(mmif_content)
128
+ tmp_path = Path(tmp.name)
129
+ output = describe_single_mmif(tmp_path)
130
+ finally:
131
+ if tmp_path and tmp_path.exists():
132
+ tmp_path.unlink()
133
+
134
+ if output:
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')
139
+
140
+
141
+ if __name__ == "__main__":
142
+ parser = prep_argparser()
143
+ args = parser.parse_args()
144
+ main(args)