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.
- {mmif_python-1.2.1 → mmif_python-1.3.0}/PKG-INFO +11 -8
- {mmif_python-1.2.1 → mmif_python-1.3.0}/README.md +9 -7
- mmif_python-1.3.0/VERSION +1 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/__init__.py +8 -8
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/serialize/__init__.py +4 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/serialize/annotation.py +2 -2
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/serialize/mmif.py +8 -8
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/serialize/model.py +4 -1
- mmif_python-1.3.0/mmif/utils/__init__.py +4 -0
- mmif_python-1.3.0/mmif/utils/cli/__init__.py +226 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/utils/cli/describe.py +56 -52
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/utils/cli/rewind.py +7 -10
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/utils/cli/source.py +4 -7
- mmif_python-1.3.0/mmif/utils/cli/summarize.py +65 -0
- mmif_python-1.3.0/mmif/utils/summarizer/__init__.py +28 -0
- mmif_python-1.3.0/mmif/utils/summarizer/config.py +69 -0
- mmif_python-1.3.0/mmif/utils/summarizer/graph.py +256 -0
- mmif_python-1.3.0/mmif/utils/summarizer/nodes.py +370 -0
- mmif_python-1.3.0/mmif/utils/summarizer/summary.py +657 -0
- mmif_python-1.3.0/mmif/utils/summarizer/utils.py +268 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/utils/video_document_helper.py +270 -21
- mmif_python-1.3.0/mmif/utils/workflow_helper.py +554 -0
- mmif_python-1.3.0/mmif/ver/__init__.py +2 -0
- mmif_python-1.3.0/mmif_docloc_http/__init__.py +30 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif_python.egg-info/PKG-INFO +11 -8
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif_python.egg-info/SOURCES.txt +7 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif_python.egg-info/requires.txt +1 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/requirements.txt +1 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/setup.py +1 -41
- {mmif_python-1.2.1 → mmif_python-1.3.0}/tests/test_serialize.py +34 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/tests/test_utils.py +300 -21
- mmif_python-1.3.0/tests/test_utils_cli.py +368 -0
- mmif_python-1.2.1/VERSION +0 -1
- mmif_python-1.2.1/mmif/utils/__init__.py +0 -0
- mmif_python-1.2.1/mmif/utils/cli/__init__.py +0 -4
- mmif_python-1.2.1/mmif/utils/workflow_helper.py +0 -466
- mmif_python-1.2.1/mmif/ver/__init__.py +0 -2
- mmif_python-1.2.1/mmif_docloc_http/__init__.py +0 -16
- mmif_python-1.2.1/tests/test_utils_cli.py +0 -306
- {mmif_python-1.2.1 → mmif_python-1.3.0}/LICENSE +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/MANIFEST.in +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/res/__init__.py +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/res/clams.vocabulary.yaml +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/res/do-not-edit.txt +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/res/mmif.json +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/serialize/view.py +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/utils/sequence_helper.py +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/utils/text_document_helper.py +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/utils/timeunit_helper.py +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/ver/do-not-edit.txt +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/vocabulary/__init__.py +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/vocabulary/annotation_types.py +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/vocabulary/base_types.py +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/vocabulary/do-not-edit.txt +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif/vocabulary/document_types.py +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif_python.egg-info/dependency_links.txt +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif_python.egg-info/entry_points.txt +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/mmif_python.egg-info/top_level.txt +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/requirements.cv +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/requirements.seq +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/setup.cfg +0 -0
- {mmif_python-1.2.1 → mmif_python-1.3.0}/tests/test_versioncompat.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
@@ -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
|
|
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,
|
|
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:
|
|
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``
|
|
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[
|
|
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[
|
|
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
|
-
|
|
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,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
|
|
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
|
-
'
|
|
36
|
-
'
|
|
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
|
-
|
|
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,
|
|
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
|
|
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=
|
|
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
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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
|
-
|
|
109
|
-
mmif_content =
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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
|
|
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__":
|