From cc525c68d484da0b1c874b92ecab3a393e3b4a32 Mon Sep 17 00:00:00 2001 From: dohyun Date: Sun, 28 Jan 2024 21:37:06 -0500 Subject: [PATCH 01/10] Initial implementation of mmif rewinder --- clams/rewind/__init__.py | 83 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 clams/rewind/__init__.py diff --git a/clams/rewind/__init__.py b/clams/rewind/__init__.py new file mode 100644 index 0000000..7055f7b --- /dev/null +++ b/clams/rewind/__init__.py @@ -0,0 +1,83 @@ +import argparse +import mmif +import json + +def read_mmif(mmif_file): + """ + Function to read mmif file as a json file and return it as a dictionary. + (Would it be better to be a mmif object?) + + :param mmif_file: file path to the mmif. + :return: dictionary with mmif data + """ + try: + with open(mmif_file, 'r') as file: + mmif_data = json.load(file) + + print(f"\nSuccessfully loaded MMIF file: {mmif_file}") + + except FileNotFoundError: + print(f"Error: MMIF file '{mmif_file}' not found.") + except json.JSONDecodeError: + print(f"Error: Invalid JSON format in MMIF file '{mmif_file}'.") + except Exception as e: + print(f"Error: An unexpected error occurred - {e}") + + return mmif_data + + +def user_choice(mmif_data): + """ + Function to ask user to choose the rewind range. + + :param mmif_data: dictionary + :return: int option number + """ + + ## Give a user options (#, "app", "timestamp") - time order + n = len(mmif_data["views"]) + i = 0 # option number + # header + print("\n"+"{:<4} {:<30} {:<100}".format("num", "timestamp", "app")) + for view in mmif_data["views"]: + if "timestamp" in view["metadata"]: + option = "{:<4} {:<30} {:<100}".format(i, view["metadata"]["timestamp"], view["metadata"]["app"]) + else: + option = "{:<4} {:<30} {:<100}".format(i, "-", view["metadata"]["app"]) + print(option) + i += 1 + + ## User input + while True: + try: + choice = int(input("\nEnter the number to delete from that point by rewinding: ")) + if 0 <= choice <= n-1: + return choice + else: + print(f"\nInvalid choice. Please enter a number between 0 and {n-1}") + except ValueError: + print("\nInvalid input. Please enter a valid number.") + + +def process_mmif_from_user_choice(mmif_data, choice): + """ + Process rewinding of mmif data from user choice and save it in as a json file. + + :param mmif_data: + :param choice: + :return: Output.mmif + """ + mmif_data["views"] = mmif_data["views"][:choice] + file_name = str(input("\nEnter the file name for the rewound mmif: ")) + with open(file_name, 'w') as json_file: + json.dump(mmif_data, json_file) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Process MMIF file.") + parser.add_argument("mmif_file", help="Path to the MMIF file") + args = parser.parse_args() + + mmif_data = read_mmif(args.mmif_file) + choice = user_choice(mmif_data) + process_mmif_from_user_choice(mmif_data, choice) From ece453f5e62041774a426cc3ba940fed3a096f34 Mon Sep 17 00:00:00 2001 From: dohyun Date: Sun, 28 Jan 2024 21:38:05 -0500 Subject: [PATCH 02/10] Options are added: -o for output file name and -p for pretty printing. --- clams/rewind/__init__.py | 75 ++++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 29 deletions(-) diff --git a/clams/rewind/__init__.py b/clams/rewind/__init__.py index 7055f7b..24d9fbf 100644 --- a/clams/rewind/__init__.py +++ b/clams/rewind/__init__.py @@ -1,14 +1,15 @@ import argparse import mmif import json +import os -def read_mmif(mmif_file): +def read_mmif(mmif_file)->mmif.Mmif: """ - Function to read mmif file as a json file and return it as a dictionary. + Function to read mmif file and return the mmif object. (Would it be better to be a mmif object?) :param mmif_file: file path to the mmif. - :return: dictionary with mmif data + :return: mmif object """ try: with open(mmif_file, 'r') as file: @@ -16,34 +17,37 @@ def read_mmif(mmif_file): print(f"\nSuccessfully loaded MMIF file: {mmif_file}") + mmif_obj = mmif.Mmif(mmif_data) + + except FileNotFoundError: print(f"Error: MMIF file '{mmif_file}' not found.") - except json.JSONDecodeError: - print(f"Error: Invalid JSON format in MMIF file '{mmif_file}'.") + + except Exception as e: print(f"Error: An unexpected error occurred - {e}") - return mmif_data + return mmif_obj -def user_choice(mmif_data): +def user_choice(mmif_obj:mmif.Mmif) -> int: """ Function to ask user to choose the rewind range. - :param mmif_data: dictionary + :param mmif_obj: mmif object :return: int option number """ ## Give a user options (#, "app", "timestamp") - time order - n = len(mmif_data["views"]) - i = 0 # option number + n = len(mmif_obj.views) + i = 0 # option number # header - print("\n"+"{:<4} {:<30} {:<100}".format("num", "timestamp", "app")) - for view in mmif_data["views"]: - if "timestamp" in view["metadata"]: - option = "{:<4} {:<30} {:<100}".format(i, view["metadata"]["timestamp"], view["metadata"]["app"]) - else: - option = "{:<4} {:<30} {:<100}".format(i, "-", view["metadata"]["app"]) + print("\n" + "{:<4} {:<30} {:<100}".format("num", "timestamp", "app")) + for view in mmif_obj.views: + option = "{:<4} {:<30} {:<100}".format(i, str(view.metadata.timestamp), str(view.metadata.app)) + + + print(option) i += 1 @@ -51,33 +55,46 @@ def user_choice(mmif_data): while True: try: choice = int(input("\nEnter the number to delete from that point by rewinding: ")) - if 0 <= choice <= n-1: + if 0 <= choice <= n - 1: return choice else: - print(f"\nInvalid choice. Please enter a number between 0 and {n-1}") + print(f"\nInvalid choice. Please enter a number between 0 and {n - 1}") except ValueError: print("\nInvalid input. Please enter a valid number.") -def process_mmif_from_user_choice(mmif_data, choice): +def process_mmif_from_user_choice(mmif_obj, choice: int, output_fp = "rewound.mmif", p=True) -> None: """ Process rewinding of mmif data from user choice and save it in as a json file. - :param mmif_data: - :param choice: - :return: Output.mmif + :param mmif_obj: mmif object + :param choice: integer to rewind from + :param output_fp: path to save the rewound output file + :return: rewound.mmif saved """ - mmif_data["views"] = mmif_data["views"][:choice] - file_name = str(input("\nEnter the file name for the rewound mmif: ")) - with open(file_name, 'w') as json_file: - json.dump(mmif_data, json_file) + n = len(mmif_obj.views) - choice + mmif_obj.views.__delete_last(n) + mmif_serialized = mmif_obj.serialize(pretty=p) + + # Check if the same file name exist in the path and avoid overwriting. + if os.path.exists(output_fp): + file_name, file_extension = os.path.splitext(output_fp) + count = 1 + while os.path.exists(f"{file_name}_{count}.mmif"): + count += 1 + output_fp = f"{file_name}_{count}.mmif" + + with open(output_fp, 'w') as mmif_file: + mmif_file.write(mmif_serialized) if __name__ == "__main__": parser = argparse.ArgumentParser(description="Process MMIF file.") parser.add_argument("mmif_file", help="Path to the MMIF file") + parser.add_argument("-o", '--output', type=str, help="Path to the rewound MMIF output file (default: rewound.mmif)") + parser.add_argument("-p", '--pretty', help="Pretty print (default: pretty=True)") args = parser.parse_args() - mmif_data = read_mmif(args.mmif_file) - choice = user_choice(mmif_data) - process_mmif_from_user_choice(mmif_data, choice) + mmif_obj = read_mmif(args.mmif_file) + choice = user_choice(mmif_obj) + process_mmif_from_user_choice(mmif_obj, choice, args.output, args.pretty) From 632d82aca12924dbbd73ab9882d2248ce3bcd415 Mon Sep 17 00:00:00 2001 From: dohyun Date: Sun, 28 Jan 2024 21:38:38 -0500 Subject: [PATCH 03/10] Review comments reflected: - changed rewinder file name taking off mmif_. - changed _delete_last function name to be single underscored. - added CLI argument for number of views to rewind. - changed read_mmif from json.load() -> file.read() --- clams/rewind/__init__.py | 52 +++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 24 deletions(-) diff --git a/clams/rewind/__init__.py b/clams/rewind/__init__.py index 24d9fbf..d825685 100644 --- a/clams/rewind/__init__.py +++ b/clams/rewind/__init__.py @@ -1,6 +1,5 @@ import argparse import mmif -import json import os def read_mmif(mmif_file)->mmif.Mmif: @@ -13,22 +12,24 @@ def read_mmif(mmif_file)->mmif.Mmif: """ try: with open(mmif_file, 'r') as file: - mmif_data = json.load(file) - - print(f"\nSuccessfully loaded MMIF file: {mmif_file}") - - mmif_obj = mmif.Mmif(mmif_data) - + mmif_obj = mmif.Mmif(file.read()) except FileNotFoundError: print(f"Error: MMIF file '{mmif_file}' not found.") - - except Exception as e: print(f"Error: An unexpected error occurred - {e}") return mmif_obj +def is_valid_choice(choice): + try: + ichoice = int(choice) + if 0 <= ichoice: + return ichoice + else: + raise ValueError(f"\nInvalid argument for -n. Please enter a positive integer.") + except ValueError: + raise argparse.ArgumentTypeError(f"\nInvalid argument for -n. Please enter a positive integer.") def user_choice(mmif_obj:mmif.Mmif) -> int: """ @@ -44,36 +45,33 @@ def user_choice(mmif_obj:mmif.Mmif) -> int: # header print("\n" + "{:<4} {:<30} {:<100}".format("num", "timestamp", "app")) for view in mmif_obj.views: - option = "{:<4} {:<30} {:<100}".format(i, str(view.metadata.timestamp), str(view.metadata.app)) - - - + option = "{:<4} {:<30} {:<100}".format(n-i, str(view.metadata.timestamp), str(view.metadata.app)) print(option) i += 1 ## User input while True: + choice = int(input("\nEnter the number to delete from that point by rewinding: ")) try: - choice = int(input("\nEnter the number to delete from that point by rewinding: ")) - if 0 <= choice <= n - 1: + if 0 <= choice <= n: return choice else: - print(f"\nInvalid choice. Please enter a number between 0 and {n - 1}") + print(f"\nInvalid choice. Please enter an integer in the range [0, {n}].") except ValueError: print("\nInvalid input. Please enter a valid number.") -def process_mmif_from_user_choice(mmif_obj, choice: int, output_fp = "rewound.mmif", p=True) -> None: +def process_mmif(mmif_obj, choice: int, output_fp = "rewound.mmif", p=True) -> None: """ Process rewinding of mmif data from user choice and save it in as a json file. :param mmif_obj: mmif object :param choice: integer to rewind from :param output_fp: path to save the rewound output file + :param p: whether using pretty printing or not :return: rewound.mmif saved """ - n = len(mmif_obj.views) - choice - mmif_obj.views.__delete_last(n) + mmif_obj.views._delete_last(choice) mmif_serialized = mmif_obj.serialize(pretty=p) # Check if the same file name exist in the path and avoid overwriting. @@ -86,15 +84,21 @@ def process_mmif_from_user_choice(mmif_obj, choice: int, output_fp = "rewound.mm with open(output_fp, 'w') as mmif_file: mmif_file.write(mmif_serialized) - + print("Successfully processed the rewind") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Process MMIF file.") parser.add_argument("mmif_file", help="Path to the MMIF file") - parser.add_argument("-o", '--output', type=str, help="Path to the rewound MMIF output file (default: rewound.mmif)") - parser.add_argument("-p", '--pretty', help="Pretty print (default: pretty=True)") + parser.add_argument("-o", '--output', default = "rewound.mmif", type=str, help="Path to the rewound MMIF output file (default: rewound.mmif)") + parser.add_argument("-p", '--pretty', default = True, type = bool, help="Pretty print (default: pretty=True)") + parser.add_argument("-n", '--number', default = "0", type = is_valid_choice, help="Number of views to rewind (default: 0)") args = parser.parse_args() mmif_obj = read_mmif(args.mmif_file) - choice = user_choice(mmif_obj) - process_mmif_from_user_choice(mmif_obj, choice, args.output, args.pretty) + + if args.number == 0: # If user doesn't know how many views to rewind, give them choices. + choice = user_choice(mmif_obj) + else: + choice = args.number + + process_mmif(mmif_obj, choice, args.output, args.pretty) From e6a044f5887e0b3123f3cb71446765a8c33a59b7 Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Sun, 28 Jan 2024 23:46:37 -0500 Subject: [PATCH 04/10] updated rewinder module ... * optimized MMIF file I/O * added rewinding by number of apps (opposed to number of views, not exposed in CLI) --- clams/rewind/__init__.py | 90 +++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 42 deletions(-) diff --git a/clams/rewind/__init__.py b/clams/rewind/__init__.py index d825685..3de52e0 100644 --- a/clams/rewind/__init__.py +++ b/clams/rewind/__init__.py @@ -1,25 +1,8 @@ import argparse -import mmif -import os - -def read_mmif(mmif_file)->mmif.Mmif: - """ - Function to read mmif file and return the mmif object. - (Would it be better to be a mmif object?) - - :param mmif_file: file path to the mmif. - :return: mmif object - """ - try: - with open(mmif_file, 'r') as file: - mmif_obj = mmif.Mmif(file.read()) +from pathlib import Path as P - except FileNotFoundError: - print(f"Error: MMIF file '{mmif_file}' not found.") - except Exception as e: - print(f"Error: An unexpected error occurred - {e}") +import mmif - return mmif_obj def is_valid_choice(choice): try: @@ -61,44 +44,67 @@ def user_choice(mmif_obj:mmif.Mmif) -> int: print("\nInvalid input. Please enter a valid number.") -def process_mmif(mmif_obj, choice: int, output_fp = "rewound.mmif", p=True) -> None: +def rewind_mmif(mmif_obj: mmif.Mmif, choice: int, choice_is_viewnum: bool = True) -> mmif.Mmif: """ - Process rewinding of mmif data from user choice and save it in as a json file. + Rewind MMIF by deleting the last N views. + The number of views to rewind is given as a number of "views", or number of "producer apps". + By default, the number argument is interpreted as the number of "views". :param mmif_obj: mmif object - :param choice: integer to rewind from - :param output_fp: path to save the rewound output file - :param p: whether using pretty printing or not - :return: rewound.mmif saved + :param choice: number of views to rewind + :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. + :return: rewound mmif object + """ - mmif_obj.views._delete_last(choice) - mmif_serialized = mmif_obj.serialize(pretty=p) + if choice_is_viewnum: + for vid in list(v.id for v in mmif_obj.views)[-1:-choice-1:-1]: + mmif_obj.views._items.pop(vid) + else: + app_count = 0 + cur_app = "" + vid_to_pop = [] + for v in reversed(mmif_obj.views): + if app_count >= choice: + break + if v.metadata.app != cur_app: + app_count += 1 + cur_app = v.metadata.app + vid_to_pop.append(v.id) + for vid in vid_to_pop: + mmif_obj.views._items.pop(vid) + return mmif_obj + + - # Check if the same file name exist in the path and avoid overwriting. - if os.path.exists(output_fp): - file_name, file_extension = os.path.splitext(output_fp) - count = 1 - while os.path.exists(f"{file_name}_{count}.mmif"): - count += 1 - output_fp = f"{file_name}_{count}.mmif" - with open(output_fp, 'w') as mmif_file: - mmif_file.write(mmif_serialized) - print("Successfully processed the rewind") if __name__ == "__main__": parser = argparse.ArgumentParser(description="Process MMIF file.") parser.add_argument("mmif_file", help="Path to the MMIF file") - parser.add_argument("-o", '--output', default = "rewound.mmif", type=str, help="Path to the rewound MMIF output file (default: rewound.mmif)") - parser.add_argument("-p", '--pretty', default = True, type = bool, help="Pretty print (default: pretty=True)") - parser.add_argument("-n", '--number', default = "0", type = is_valid_choice, help="Number of views to rewind (default: 0)") + parser.add_argument("-o", '--output', default="rewound.mmif", type=str, help="Path to the rewound MMIF output file (default: rewound.mmif)") + parser.add_argument("-p", '--pretty', action='store_true', help="Pretty print (default: pretty=True)") + parser.add_argument("-n", '--number', default="0", type=is_valid_choice, help="Number of views to rewind (default: 0)") args = parser.parse_args() - mmif_obj = read_mmif(args.mmif_file) + mmif_obj = mmif.Mmif(open(args.mmif_file).read()) if args.number == 0: # If user doesn't know how many views to rewind, give them choices. choice = user_choice(mmif_obj) else: choice = args.number - process_mmif(mmif_obj, choice, args.output, args.pretty) + + # Check if the same file name exist in the path and avoid overwriting. + output_fp = P(args.output) + if output_fp.is_file(): + parent = output_fp.parent + stem = output_fp.stem + suffix = output_fp.suffix + count = 1 + while (parent / f"{stem}_{count}{suffix}").is_file(): + count += 1 + output_fp = parent / f"{stem}_{count}{suffix}" + + with open(output_fp, 'w') as mmif_file: + mmif_file.write(rewind_mmif(mmif_obj, choice).serialize(pretty=args.pretty)) + From 37e58e87ac2e15c52c302d85c63e5705f2e0e130 Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Sun, 28 Jan 2024 23:31:34 -0500 Subject: [PATCH 05/10] merged `source` and `rewind` CLI under `mmif_utils` package --- clams/__init__.py | 7 +++-- .../__init__.py => mmif_utils/rewind.py} | 26 ++++++++++++++++--- .../__init__.py => mmif_utils/source.py} | 0 tests/test_clamscli.py | 4 +-- 4 files changed, 29 insertions(+), 8 deletions(-) rename clams/{rewind/__init__.py => mmif_utils/rewind.py} (82%) rename clams/{source/__init__.py => mmif_utils/source.py} (100%) diff --git a/clams/__init__.py b/clams/__init__.py index 7ead7de..6943815 100644 --- a/clams/__init__.py +++ b/clams/__init__.py @@ -3,7 +3,8 @@ from mmif import __specver__ from clams import develop -from clams import source +from clams.mmif_utils import source +from clams.mmif_utils import rewind from clams.app import * from clams.app import __all__ as app_all from clams.appmetadata import AppMetadata @@ -23,7 +24,7 @@ def prep_argparser(): version=version_template.format(__version__, __specver__) ) subparsers = parser.add_subparsers(title='sub-command', dest='subcmd') - for subcmd_module in [source, develop]: + for subcmd_module in [source, rewind, develop]: subcmd_name = subcmd_module.__name__.rsplit('.')[-1] subcmd_parser = subcmd_module.prep_argparser(add_help=False) subparsers.add_parser(subcmd_name, parents=[subcmd_parser], @@ -42,5 +43,7 @@ def cli(): args = parser.parse_args() if args.subcmd == 'source': source.main(args) + if args.subcmd == 'rewind': + rewind.main(args) if args.subcmd == 'develop': develop.main(args) diff --git a/clams/rewind/__init__.py b/clams/mmif_utils/rewind.py similarity index 82% rename from clams/rewind/__init__.py rename to clams/mmif_utils/rewind.py index 3de52e0..d60d8af 100644 --- a/clams/rewind/__init__.py +++ b/clams/mmif_utils/rewind.py @@ -1,4 +1,5 @@ import argparse +import textwrap from pathlib import Path as P import mmif @@ -75,17 +76,28 @@ def rewind_mmif(mmif_obj: mmif.Mmif, choice: int, choice_is_viewnum: bool = True return mmif_obj +def describe_argparser(): + """ + returns two strings: one-line description of the argparser, and addition material, + which will be shown in `clams --help` and `clams --help`, respectively. + """ + oneliner = 'provides CLI to rewind a MMIF from a CLAMS pipeline.' + additional = textwrap.dedent(""" + MMIF rewinder rewinds a MMIF by deleting the last N views. + N can be specified as a number of views, or a number of producer apps. """) + return oneliner, oneliner + '\n\n' + additional - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Process MMIF file.") +def prep_argparser(**kwargs): + parser = argparse.ArgumentParser(description=describe_argparser()[1], formatter_class=argparse.RawDescriptionHelpFormatter, **kwargs) parser.add_argument("mmif_file", help="Path to the MMIF file") parser.add_argument("-o", '--output', default="rewound.mmif", type=str, help="Path to the rewound MMIF output file (default: rewound.mmif)") parser.add_argument("-p", '--pretty', action='store_true', help="Pretty print (default: pretty=True)") parser.add_argument("-n", '--number', default="0", type=is_valid_choice, help="Number of views to rewind (default: 0)") - args = parser.parse_args() + return parser + +def main(args): mmif_obj = mmif.Mmif(open(args.mmif_file).read()) if args.number == 0: # If user doesn't know how many views to rewind, give them choices. @@ -108,3 +120,9 @@ def rewind_mmif(mmif_obj: mmif.Mmif, choice: int, choice_is_viewnum: bool = True with open(output_fp, 'w') as mmif_file: mmif_file.write(rewind_mmif(mmif_obj, choice).serialize(pretty=args.pretty)) + +if __name__ == "__main__": + parser = prep_argparser() + args = parser.parse_args() + main(args) + diff --git a/clams/source/__init__.py b/clams/mmif_utils/source.py similarity index 100% rename from clams/source/__init__.py rename to clams/mmif_utils/source.py diff --git a/tests/test_clamscli.py b/tests/test_clamscli.py index 88b241f..c89a4db 100644 --- a/tests/test_clamscli.py +++ b/tests/test_clamscli.py @@ -3,7 +3,7 @@ import unittest import contextlib import clams -from clams import source +from clams import mmif_utils from mmif.serialize import Mmif @@ -44,7 +44,7 @@ def generate_source_mmif(self): args = self.parser.parse_args(self.get_params()) args.output = os.devnull - return source.main(args) + return mmif_utils.main(args) def test_accept_file_paths(self): self.docs.append("video:/a/b/c.mp4") From 79b56f4ed1227416a569c32a4c95b316cda9cbb1 Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Sun, 28 Jan 2024 23:50:27 -0500 Subject: [PATCH 06/10] updated rewinder CLI for stdin/out and picker mode argument --- clams/mmif_utils/rewind.py | 41 +++++++++++++++++++++----------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/clams/mmif_utils/rewind.py b/clams/mmif_utils/rewind.py index d60d8af..8025860 100644 --- a/clams/mmif_utils/rewind.py +++ b/clams/mmif_utils/rewind.py @@ -1,4 +1,5 @@ import argparse +import sys import textwrap from pathlib import Path as P @@ -90,15 +91,16 @@ def describe_argparser(): def prep_argparser(**kwargs): parser = argparse.ArgumentParser(description=describe_argparser()[1], formatter_class=argparse.RawDescriptionHelpFormatter, **kwargs) - parser.add_argument("mmif_file", help="Path to the MMIF file") - parser.add_argument("-o", '--output', default="rewound.mmif", type=str, help="Path to the rewound MMIF output file (default: rewound.mmif)") - parser.add_argument("-p", '--pretty', action='store_true', help="Pretty print (default: pretty=True)") - parser.add_argument("-n", '--number', default="0", type=is_valid_choice, help="Number of views to rewind (default: 0)") + parser.add_argument("mmif_file", nargs=1, help="Path to the input MMIF file, or '-' to read from stdin.") + parser.add_argument("-o", '--output', default=None, metavar="PATH", help="Path to the rewound MMIF output file. When not given, the rewound MMIF is printed to stdout.") + parser.add_argument("-p", '--pretty', action='store_true', help="Pretty-print rewound MMIF. True by default") + parser.add_argument("-n", '--number', default="0", type=is_valid_choice, help="Number of views to rewind (default: interactive mode)") + parser.add_argument("-m", '--mode', choices=['app', 'view'], default='view', help="Number of views to rewind (default: interactive mode)") return parser def main(args): - mmif_obj = mmif.Mmif(open(args.mmif_file).read()) + mmif_obj = mmif.Mmif(sys.stdin) if args.mmif_file[0] == '-' else mmif.Mmif(open(args.mmif_file[0]).read()) if args.number == 0: # If user doesn't know how many views to rewind, give them choices. choice = user_choice(mmif_obj) @@ -106,19 +108,22 @@ def main(args): choice = args.number - # Check if the same file name exist in the path and avoid overwriting. - output_fp = P(args.output) - if output_fp.is_file(): - parent = output_fp.parent - stem = output_fp.stem - suffix = output_fp.suffix - count = 1 - while (parent / f"{stem}_{count}{suffix}").is_file(): - count += 1 - output_fp = parent / f"{stem}_{count}{suffix}" - - with open(output_fp, 'w') as mmif_file: - mmif_file.write(rewind_mmif(mmif_obj, choice).serialize(pretty=args.pretty)) + if args.output: + # Check if the same file name exist in the path and avoid overwriting. + output_fp = P(args.output) + if output_fp.is_file(): + parent = output_fp.parent + stem = output_fp.stem + suffix = output_fp.suffix + count = 1 + while (parent / f"{stem}_{count}{suffix}").is_file(): + count += 1 + output_fp = parent / f"{stem}_{count}{suffix}" + + out_f = open(output_fp, 'w') + else: + out_f = sys.stdout + out_f.write(rewind_mmif(mmif_obj, choice, args.mode == 'view').serialize(pretty=args.pretty)) if __name__ == "__main__": From 6657a364f6a86796bb0b54d3dfa0cc1a718e1157 Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Sun, 28 Jan 2024 23:54:36 -0500 Subject: [PATCH 07/10] added a placeholder for test suite for rewinder --- tests/test_clamscli.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_clamscli.py b/tests/test_clamscli.py index c89a4db..a4c00e1 100644 --- a/tests/test_clamscli.py +++ b/tests/test_clamscli.py @@ -105,5 +105,8 @@ def test_generate_mixed_scheme(self): self.assertTrue('file' in schemes) +class TestRewind(unittest.TestCase): + pass + if __name__ == '__main__': unittest.main() From e4dad60caebb42cc97d63038d7e7f86657f6272d Mon Sep 17 00:00:00 2001 From: Dean Cahill Date: Mon, 29 Jan 2024 15:30:01 -0500 Subject: [PATCH 08/10] basic rewinder tests --- tests/test_clamscli.py | 120 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 115 insertions(+), 5 deletions(-) diff --git a/tests/test_clamscli.py b/tests/test_clamscli.py index a4c00e1..f0c0ab5 100644 --- a/tests/test_clamscli.py +++ b/tests/test_clamscli.py @@ -1,10 +1,15 @@ +import contextlib +import copy import io import os import unittest -import contextlib -import clams -from clams import mmif_utils + from mmif.serialize import Mmif +from mmif.vocabulary import DocumentTypes, AnnotationTypes + +import clams +from clams.mmif_utils import rewind +from clams.mmif_utils import source class TestCli(unittest.TestCase): @@ -44,7 +49,7 @@ def generate_source_mmif(self): args = self.parser.parse_args(self.get_params()) args.output = os.devnull - return mmif_utils.main(args) + return source.main(args) def test_accept_file_paths(self): self.docs.append("video:/a/b/c.mp4") @@ -106,7 +111,112 @@ def test_generate_mixed_scheme(self): class TestRewind(unittest.TestCase): - pass + def setUp(self): + self.dummy_app_one = ExampleApp() + self.dummy_app_two = ExampleApp() + + # mmif we add views to + self.mmif_one = Mmif( + { + "metadata": {"mmif": "http://mmif.clams.ai/1.0.0"}, + "documents": [], + "views": [], + } + ) + + # baseline empty mmif for comparison + self.empty_mmif = Mmif( + { + "metadata": {"mmif": "http://mmif.clams.ai/1.0.0"}, + "documents": [], + "views": [], + } + ) + + def test_view_rewind(self): + """ + Tests the use of "view-rewiding" to remove multiple views from a single app. + """ + # Regular Case + mmif_added_views = self.dummy_app_one.mmif_add_views(self.mmif_one, 10) + removed_views = rewind.rewind_mmif(mmif_added_views, 10) + + # Assertions + self.assertEqual(removed_views.__annotations__, self.empty_mmif.__annotations__) + self.assertEqual(removed_views.views, self.empty_mmif.views) + + # Edge Cases + # TODO(DeanCahill@01/29/24) - more test conditions? + + def test_app_rewind(self): + # Regular Case + app_one_out = self.dummy_app_one.mmif_add_views(self.mmif_one, 3) + copy_one = copy.deepcopy(app_one_out) # deep copy for later comparison + + app_two_out = self.dummy_app_two.mmif_add_views(app_one_out, 3) + removed_views = rewind.rewind_mmif(app_two_out, 1, choice_is_viewnum=False) + + # Assertions + self.assertEqual(removed_views.__annotations__, copy_one.__annotations__) + self.assertTrue(compare_views(removed_views, copy_one)) + + # Edge Cases + # TODO(DeanCahill@01/29/24) - more test conditions? + + +def compare_views(a: Mmif, b: Mmif) -> bool: + perfect_match = True + for view_a, view_b in zip(a.views, b.views): + if view_a != view_b: + perfect_match = False + return perfect_match + + +class ExampleApp(clams.app.ClamsApp): + """This is a barebones implementation of a CLAMS App + used to generate simple Views within a mmif object + for testing purposes. The three methods here all streamline + the mmif annotation process for the purposes of repeated insertion + and removal. + """ + + app_version = "lorem_ipsum" + + def _appmetadata(self): + pass + + def _annotate(self, mmif: Mmif, message: str, idx: int, **kwargs): + if type(mmif) is not Mmif: + mmif_obj = Mmif(mmif, validate=False) + else: + mmif_obj = mmif + + new_view = mmif_obj.new_view() + self.sign_view(new_view, runtime_conf=kwargs) + self.gen_annotate(new_view, message, idx) + + d1 = DocumentTypes.VideoDocument + d2 = DocumentTypes.from_str(f"{str(d1)[:-1]}99") + if mmif.get_documents_by_type(d2): + new_view.new_annotation(AnnotationTypes.TimePoint, "tp1") + if "raise_error" in kwargs and kwargs["raise_error"]: + raise ValueError + return mmif + + def gen_annotate(self, mmif_view, message, idx=0): + mmif_view.new_contain( + AnnotationTypes.TimeFrame, **{"producer": "dummy-producer"} + ) + ann = mmif_view.new_annotation( + AnnotationTypes.TimeFrame, "a1", start=10, end=99 + ) + ann.add_property("f1", message) + + def mmif_add_views(self, mmif_obj, idx: int): + """Helper Function to add an arbitrary number of views to a mmif""" + for i in range(idx): + mmif_obj = self._annotate(mmif_obj, message=f"message {i}", idx=idx) + return mmif_obj if __name__ == '__main__': unittest.main() From 3e399ab1fedd0d6b3abde514a5fec8b98272ae2a Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Wed, 31 Jan 2024 12:54:20 -0500 Subject: [PATCH 09/10] fixed bugs in rewinder --- clams/mmif_utils/__init__.py | 3 +++ clams/mmif_utils/rewind.py | 6 ++++-- tests/test_clamscli.py | 35 ++++++++++++++--------------------- 3 files changed, 21 insertions(+), 23 deletions(-) create mode 100644 clams/mmif_utils/__init__.py diff --git a/clams/mmif_utils/__init__.py b/clams/mmif_utils/__init__.py new file mode 100644 index 0000000..4ece749 --- /dev/null +++ b/clams/mmif_utils/__init__.py @@ -0,0 +1,3 @@ +from clams.mmif_utils import rewind +from clams.mmif_utils import source + diff --git a/clams/mmif_utils/rewind.py b/clams/mmif_utils/rewind.py index 8025860..63bb3ea 100644 --- a/clams/mmif_utils/rewind.py +++ b/clams/mmif_utils/rewind.py @@ -50,7 +50,9 @@ def rewind_mmif(mmif_obj: mmif.Mmif, choice: int, choice_is_viewnum: bool = True """ Rewind MMIF by deleting the last N views. The number of views to rewind is given as a number of "views", or number of "producer apps". - By default, the number argument is interpreted as the number of "views". + By default, the number argument is interpreted as the number of "views". + Note that when the same app is repeatedly run in a CLAMS pipeline and produces multiple views in a row, + rewinding in "app" mode will rewind all those views at once. :param mmif_obj: mmif object :param choice: number of views to rewind @@ -66,12 +68,12 @@ def rewind_mmif(mmif_obj: mmif.Mmif, choice: int, choice_is_viewnum: bool = True cur_app = "" vid_to_pop = [] for v in reversed(mmif_obj.views): + vid_to_pop.append(v.id) if app_count >= choice: break if v.metadata.app != cur_app: app_count += 1 cur_app = v.metadata.app - vid_to_pop.append(v.id) for vid in vid_to_pop: mmif_obj.views._items.pop(vid) return mmif_obj diff --git a/tests/test_clamscli.py b/tests/test_clamscli.py index f0c0ab5..89a3deb 100644 --- a/tests/test_clamscli.py +++ b/tests/test_clamscli.py @@ -113,7 +113,9 @@ def test_generate_mixed_scheme(self): class TestRewind(unittest.TestCase): def setUp(self): self.dummy_app_one = ExampleApp() + self.dummy_app_one.metadata.identifier = "dummy_app_one" self.dummy_app_two = ExampleApp() + self.dummy_app_two.metadata.identifier = "dummy_app_two" # mmif we add views to self.mmif_one = Mmif( @@ -139,30 +141,21 @@ def test_view_rewind(self): """ # Regular Case mmif_added_views = self.dummy_app_one.mmif_add_views(self.mmif_one, 10) - removed_views = rewind.rewind_mmif(mmif_added_views, 10) - - # Assertions - self.assertEqual(removed_views.__annotations__, self.empty_mmif.__annotations__) - self.assertEqual(removed_views.views, self.empty_mmif.views) - - # Edge Cases - # TODO(DeanCahill@01/29/24) - more test conditions? + self.assertEqual(len(mmif_added_views.views), 10) + rewound = rewind.rewind_mmif(mmif_added_views, 5) + self.assertEqual(len(rewound.views), 5) + # rewinding is done "in-place" + self.assertEqual(len(rewound.views), len(mmif_added_views.views)) def test_app_rewind(self): # Regular Case - app_one_out = self.dummy_app_one.mmif_add_views(self.mmif_one, 3) - copy_one = copy.deepcopy(app_one_out) # deep copy for later comparison - - app_two_out = self.dummy_app_two.mmif_add_views(app_one_out, 3) - removed_views = rewind.rewind_mmif(app_two_out, 1, choice_is_viewnum=False) - - # Assertions - self.assertEqual(removed_views.__annotations__, copy_one.__annotations__) - self.assertTrue(compare_views(removed_views, copy_one)) - - # Edge Cases - # TODO(DeanCahill@01/29/24) - more test conditions? - + app_one_views = 3 + app_two_views = 2 + app_one_out = self.dummy_app_one.mmif_add_views(self.mmif_one, app_one_views) + app_two_out = self.dummy_app_two.mmif_add_views(app_one_out, app_two_views) + self.assertEqual(len(app_two_out.views), app_one_views + app_two_views) + rewound = rewind.rewind_mmif(app_two_out, 1, choice_is_viewnum=False) + self.assertEqual(len(rewound.views), app_one_views) def compare_views(a: Mmif, b: Mmif) -> bool: perfect_match = True From 4e82ce85428152250d68a0d7d98bcacd681832c8 Mon Sep 17 00:00:00 2001 From: Keigh Rim Date: Wed, 31 Jan 2024 12:25:49 -0500 Subject: [PATCH 10/10] suppressed sign_view warning when irrelevant --- clams/app/__init__.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/clams/app/__init__.py b/clams/app/__init__.py index 22ff159..c0074be 100644 --- a/clams/app/__init__.py +++ b/clams/app/__init__.py @@ -193,10 +193,11 @@ def sign_view(self, view: View, runtime_conf: Optional[dict] = None) -> None: :param runtime_conf: runtime configuration of the app as k-v pairs """ # TODO (krim @ 8/2/23): once all devs understood this change, make runtime_conf a required argument - warnings.warn("`runtime_conf` argument for ClamsApp.sign_view() will " - "no longer be optional in the future. Please just pass " - "`runtime_params` from _annotate() method.", - FutureWarning, stacklevel=2) + if runtime_conf is None: + warnings.warn("`runtime_conf` argument for ClamsApp.sign_view() will " + "no longer be optional in the future. Please just pass " + "`runtime_params` from _annotate() method.", + FutureWarning, stacklevel=2) view.metadata.app = self.metadata.identifier if runtime_conf is not None: if self._RAW_PARAMS_KEY in runtime_conf: