Skip to content

Commit 969f393

Browse files
authored
Merge pull request #317 from clamsproject/316-migrate-cli-fromclams-tommif
added `mmif`, `mmif source`, `mmif rewind` commands migrated from `clams-python` SDK
2 parents 8a6a942 + 11015e8 commit 969f393

File tree

6 files changed

+631
-0
lines changed

6 files changed

+631
-0
lines changed

mmif/__init__.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,66 @@
1+
import argparse
2+
import importlib
13
import importlib.resources
4+
import pkgutil
5+
import sys
26

37
# DO NOT CHANGE THIS ORDER, important to prevent circular imports
48
from mmif.ver import __version__
59
from mmif.ver import __specver__
610
from mmif.vocabulary import *
711
from mmif.serialize import *
12+
from mmif.utils.cli import rewind
13+
from mmif.utils.cli import source
814

915
_res_pkg = 'res'
1016
_ver_pkg = 'ver'
1117
_vocabulary_pkg = 'vocabulary'
1218
_schema_res_name = 'mmif.json'
19+
version_template = "{} (based on MMIF spec: {})"
1320

1421

1522
def get_mmif_json_schema():
1623
# TODO (krim @ 7/14/23): use `as_file` after dropping support for Python 3.8
1724
return importlib.resources.read_text(f'{__package__}.{_res_pkg}', _schema_res_name)
25+
26+
27+
def find_all_modules(pkgname):
28+
parent = importlib.import_module(pkgname)
29+
if not hasattr(parent, '__path__'):
30+
raise ImportError(f"Error: '{pkgname}' is not a package.")
31+
for importer, module, ispkg in pkgutil.walk_packages(parent.__path__, parent.__name__ + '.'):
32+
if not ispkg: # Only process modules, not subpackages themselves
33+
yield importlib.import_module(module)
34+
35+
36+
def prep_argparser_and_subcmds():
37+
parser = argparse.ArgumentParser()
38+
parser.add_argument(
39+
'-v', '--version',
40+
action='version',
41+
version=version_template.format(__version__, __specver__)
42+
)
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 = {}
50+
for cli_module in find_all_modules('mmif.utils.cli'):
51+
cli_module_name = cli_module.__name__.rsplit('.')[-1]
52+
cli_modules[cli_module_name] = cli_module
53+
subcmd_parser = cli_module.prep_argparser(add_help=False)
54+
subparsers.add_parser(cli_module_name, parents=[subcmd_parser],
55+
help=cli_module.describe_argparser()[0],
56+
description=cli_module.describe_argparser()[1],
57+
formatter_class=argparse.RawDescriptionHelpFormatter,
58+
)
59+
if len(sys.argv) == 1:
60+
parser.print_help(sys.stderr)
61+
sys.exit(1)
62+
args = parser.parse_args()
63+
if args.subcmd not in cli_modules:
64+
parser.print_help(sys.stderr)
65+
else:
66+
cli_modules[args.subcmd].main(args)

mmif/utils/cli/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from mmif.utils.cli import rewind
2+
from mmif.utils.cli import source
3+

mmif/utils/cli/rewind.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import argparse
2+
import sys
3+
import textwrap
4+
5+
import mmif
6+
7+
8+
def prompt_user(mmif_obj: mmif.Mmif) -> int:
9+
"""
10+
Function to ask user to choose the rewind range.
11+
"""
12+
13+
## Give a user options (#, "app", "timestamp") - time order
14+
n = len(mmif_obj.views)
15+
i = 0 # option number
16+
aname = ""
17+
a = 0
18+
# header
19+
print("\n" + "{:<8} {:<8} {:<30} {:<100}".format("view-num", "app-num", "timestamp", "app"))
20+
for view in reversed(mmif_obj.views):
21+
if view.metadata.app != aname:
22+
aname = view.metadata.app
23+
a += 1
24+
i += 1
25+
print("{:<8} {:<8} {:<30} {:<100}".format(i, a, str(view.metadata.timestamp), str(view.metadata.app)))
26+
27+
## User input
28+
return int(input("\nEnter the number to delete from that point by rewinding: "))
29+
30+
31+
def rewind_mmif(mmif_obj: mmif.Mmif, choice: int, choice_is_viewnum: bool = True) -> mmif.Mmif:
32+
"""
33+
Rewind MMIF by deleting the last N views.
34+
The number of views to rewind is given as a number of "views", or number of "producer apps".
35+
By default, the number argument is interpreted as the number of "views".
36+
Note that when the same app is repeatedly run in a CLAMS pipeline and produces multiple views in a row,
37+
rewinding in "app" mode will rewind all those views at once.
38+
39+
:param mmif_obj: mmif object
40+
:param choice: number of views to rewind
41+
:param choice_is_viewnum: if True, choice is the number of views to rewind. If False, choice is the number of producer apps to rewind.
42+
:return: rewound mmif object
43+
44+
"""
45+
if choice_is_viewnum:
46+
for vid in list(v.id for v in mmif_obj.views)[-1:-choice-1:-1]:
47+
mmif_obj.views._items.pop(vid)
48+
else:
49+
app_count = 0
50+
cur_app = ""
51+
vid_to_pop = []
52+
for v in reversed(mmif_obj.views):
53+
vid_to_pop.append(v.id)
54+
if app_count >= choice:
55+
break
56+
if v.metadata.app != cur_app:
57+
app_count += 1
58+
cur_app = v.metadata.app
59+
for vid in vid_to_pop:
60+
mmif_obj.views._items.pop(vid)
61+
return mmif_obj
62+
63+
64+
def describe_argparser():
65+
"""
66+
returns two strings: one-line description of the argparser, and addition material,
67+
which will be shown in `clams --help` and `clams <subcmd> --help`, respectively.
68+
"""
69+
oneliner = 'provides CLI to rewind a MMIF from a CLAMS pipeline.'
70+
additional = textwrap.dedent("""
71+
MMIF rewinder rewinds a MMIF by deleting the last N views.
72+
N can be specified as a number of views, or a number of producer apps. """)
73+
return oneliner, oneliner + '\n\n' + additional
74+
75+
76+
def prep_argparser(**kwargs):
77+
parser = argparse.ArgumentParser(description=describe_argparser()[1],
78+
formatter_class=argparse.RawDescriptionHelpFormatter, **kwargs)
79+
parser.add_argument("IN_MMIF_FILE",
80+
nargs="?", type=argparse.FileType("r"),
81+
default=None if sys.stdin.isatty() else sys.stdin,
82+
help='input MMIF file path, or STDIN if `-` or not provided.')
83+
parser.add_argument("OUT_MMIF_FILE",
84+
nargs="?", type=argparse.FileType("w"),
85+
default=sys.stdout,
86+
help='output MMIF file path, or STDOUT if `-` or not provided.')
87+
parser.add_argument("-p", '--pretty', action='store_true',
88+
help="Pretty-print rewound MMIF")
89+
parser.add_argument("-n", '--number', default="0", type=int,
90+
help="Number of views or apps to rewind, must be a positive integer. "
91+
"If 0, the user will be prompted to choose. (default: 0)")
92+
parser.add_argument("-m", '--mode', choices=['app', 'view'], default='view',
93+
help="Choose to rewind by number of views or number of producer apps. (default: view)")
94+
return parser
95+
96+
97+
def main(args):
98+
mmif_obj = mmif.Mmif(args.IN_MMIF_FILE.read())
99+
100+
if args.number == 0: # If user doesn't know how many views to rewind, give them choices.
101+
choice = prompt_user(mmif_obj)
102+
else:
103+
choice = args.number
104+
if not isinstance(choice, int) or choice <= 0:
105+
raise ValueError(f"Only can rewind by a positive number of views. Got {choice}.")
106+
107+
args.OUT_MMIF_FILE.write(rewind_mmif(mmif_obj, choice, args.mode == 'view').serialize(pretty=args.pretty))
108+
109+
110+
if __name__ == "__main__":
111+
parser = prep_argparser()
112+
args = parser.parse_args()
113+
main(args)

0 commit comments

Comments
 (0)