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.
- {mmif_python-1.2.0 → mmif_python-1.3.0}/PKG-INFO +11 -8
- {mmif_python-1.2.0 → mmif_python-1.3.0}/README.md +9 -7
- mmif_python-1.3.0/VERSION +1 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/__init__.py +8 -8
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/serialize/__init__.py +4 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/serialize/annotation.py +3 -3
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/serialize/mmif.py +9 -9
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/serialize/model.py +4 -1
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/serialize/view.py +1 -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.3.0/mmif/utils/cli/describe.py +144 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/utils/cli/rewind.py +22 -32
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/utils/cli/source.py +5 -8
- 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.0 → 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.0 → mmif_python-1.3.0}/mmif_python.egg-info/PKG-INFO +11 -8
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif_python.egg-info/SOURCES.txt +8 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif_python.egg-info/requires.txt +1 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/requirements.txt +1 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/setup.py +1 -41
- {mmif_python-1.2.0 → mmif_python-1.3.0}/tests/test_serialize.py +34 -0
- mmif_python-1.3.0/tests/test_utils.py +566 -0
- mmif_python-1.3.0/tests/test_utils_cli.py +368 -0
- mmif_python-1.2.0/VERSION +0 -1
- mmif_python-1.2.0/mmif/utils/__init__.py +0 -0
- mmif_python-1.2.0/mmif/utils/cli/__init__.py +0 -4
- mmif_python-1.2.0/mmif/utils/cli/describe.py +0 -333
- mmif_python-1.2.0/mmif/ver/__init__.py +0 -2
- mmif_python-1.2.0/mmif_docloc_http/__init__.py +0 -16
- mmif_python-1.2.0/tests/test_utils.py +0 -224
- mmif_python-1.2.0/tests/test_utils_cli.py +0 -170
- {mmif_python-1.2.0 → mmif_python-1.3.0}/LICENSE +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/MANIFEST.in +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/res/__init__.py +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/res/clams.vocabulary.yaml +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/res/do-not-edit.txt +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/res/mmif.json +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/utils/sequence_helper.py +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/utils/text_document_helper.py +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/utils/timeunit_helper.py +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/ver/do-not-edit.txt +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/vocabulary/__init__.py +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/vocabulary/annotation_types.py +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/vocabulary/base_types.py +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/vocabulary/do-not-edit.txt +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif/vocabulary/document_types.py +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif_python.egg-info/dependency_links.txt +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif_python.egg-info/entry_points.txt +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/mmif_python.egg-info/top_level.txt +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/requirements.cv +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/requirements.seq +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/setup.cfg +0 -0
- {mmif_python-1.2.0 → mmif_python-1.3.0}/tests/test_versioncompat.py +0 -0
- {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.
|
|
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)
|
|
@@ -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
|
|
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
|
|
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
|
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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,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)
|