diff --git a/shared/workspaces/workspaces/capability/enums.py b/shared/workspaces/workspaces/capability/enums.py index 7244de245d7abbcdb699f3a4056313524d6d709b..35427ed2519d39543dcaa3a3924544998e5b127c 100644 --- a/shared/workspaces/workspaces/capability/enums.py +++ b/shared/workspaces/workspaces/capability/enums.py @@ -14,6 +14,7 @@ class CapabilityStepType(Enum): AwaitProduct = 3 AwaitParameter = 4 AwaitLargeAllocApproval = 5 + Invalid = -1 @classmethod def from_string(cls, string: str) -> CapabilityStepType: @@ -31,7 +32,7 @@ class CapabilityStepType(Enum): "await-parameter": cls.AwaitParameter, "await-large-alloc-approval": cls.AwaitLargeAllocApproval, } - return strings[string] + return strings.get(string, cls.Invalid) class CapabilityEventType(Enum): diff --git a/shared/workspaces/workspaces/capability/helpers.py b/shared/workspaces/workspaces/capability/helpers.py index 13b42bc83259c5145d4715ed57b32e34d5c57755..24c71836e4ab9f296dbf369ffbf7fc4940433ddb 100644 --- a/shared/workspaces/workspaces/capability/helpers.py +++ b/shared/workspaces/workspaces/capability/helpers.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import re from typing import Iterator, List, Optional from workspaces.capability.enums import CapabilityStepType @@ -13,6 +14,10 @@ from workspaces.capability.schema_interfaces import CapabilityExecutionIF from workspaces.capability.services.interfaces import CapabilityEngineIF +class MalformedCapabilityStep(ValueError): + pass + + class CapabilityStep(CapabilityStepIF): """ Class that represents a step in a capability sequence. A step is of a certain type and @@ -31,25 +36,63 @@ class CapabilityStep(CapabilityStepIF): self.step_value = step_value self.step_args = step_args + @staticmethod + def parse_args(args_string: str) -> Optional[List[str]]: + """ + Parse a string of the form [arg-1, arg_2, -a 3], etc. into a list of those args + + :param args_string: Args as a string + :return: List of given args or None if no args given + """ + args_string = args_string.replace("[", "").replace("]", "") + return args_string.split(", ") if args_string else None + @classmethod - def from_str(cls, step_string: str): + def from_str(cls, step_string: str) -> CapabilityStep: """ Create CapabilityStep from string containing space-separated type, value, and args :param step_string: String of capability step, e.g. "PrepareAndRunWorkflow null" :return: CapabilityStep of given string """ - step_list = step_string.split(" ") - step_type = CapabilityStepType.from_string(step_list[0]) - if step_type is CapabilityStepType.PrepareAndRunWorkflow: - step_value = step_list[1] - step_args = step_list[2] if len(step_list) == 3 else None + # === Regexes === + # Matches a string with letters, digits, underscores, and hyphens, labeled steptype + # Ex: prepare-and-run-workflow, await-qa + r_step_type = r"(?P<steptype>[\w-]+)" + # Matches a string with letters, digits, underscores, and these specials: [./:], labeled stepval + # Ex: cal://alma/..., workflow_name, null + r_step_val = r"(?P<stepval>[\w\-./:]+)" + # Matches a string surrounded by [], with 0 or more of the same string as above (plus spaces), separated by + # ", " inside them + # Ex: [-f, -l path/to/location, arg_value], [arg], [] + r_step_args = r"(?P<stepargs>\[(?:[\w\-./: ]+(?:, )?)*\])" + # Matches a string taking the form of a full capability step + # Ex: prepare-and-run-workflow null [-g], prepare-and-run-workflow download, await-qa + r_step = rf"^{r_step_type} ?{r_step_val}? ?{r_step_args}?$" + + if match := re.search(r_step, step_string): + groups = match.groupdict() + step_type = groups.get("steptype", None) + step_value = groups.get("stepval", None) + step_args = groups.get("stepargs", None) + if step_type: + # Translate step_type from str to CapabilityStepType + step_type = CapabilityStepType.from_string(step_type) + + if step_type is CapabilityStepType.Invalid: + raise MalformedCapabilityStep(f"Step type {step_type} invalid.") + else: + raise MalformedCapabilityStep( + f"Capability step {step_string} is malformed. Step type not found when it is required." + ) + if step_args: + step_args = cls.parse_args(step_args) + return cls.TYPES[step_type](step_type, step_value, step_args) else: - # Any other step type - step_value = step_list[1] if len(step_list) == 2 else None - step_args = None - - return cls.TYPES[step_type](step_type, step_value, step_args) + # Step string not well-formatted + raise MalformedCapabilityStep( + f"Capability step {step_string} is malformed." + ) def __str__(self): string = f"{self.step_type.name}" @@ -122,7 +165,9 @@ class PrepareAndRunWorkflow(CapabilityStep): # DO NOT TAKE THIS OUT! Python will yell at you. parameters = json.dumps(parameters) workflow_args = json.loads(parameters) - engine.submit_workflow_request(execution.id, workflow_name, workflow_args, files) + engine.submit_workflow_request( + execution.id, workflow_name, workflow_args, files + ) class AwaitQa(CapabilityStep): diff --git a/shared/workspaces/workspaces/capability/test/test_helpers.py b/shared/workspaces/workspaces/capability/test/test_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..38aafc6f267bf300989d3184c79335842b3ab134 --- /dev/null +++ b/shared/workspaces/workspaces/capability/test/test_helpers.py @@ -0,0 +1,68 @@ +import pytest + +from workspaces.capability.enums import CapabilityStepType +from workspaces.capability.helpers import CapabilityStep, MalformedCapabilityStep + + +def test_capability_step_from_str(): + """ + Tests that a capability step can be correctly parsed from a string + """ + step_with_val_and_args = "prepare-and-run-workflow deliver [-r, -l ., shared/workspaces/test/test_data/spool/724126739/17A-109.sb33151331.eb33786546.57892.65940042824]" + step_with_val_and_one_arg = "prepare-and-run-workflow null [-g]" + step_with_val_and_empty_args = "prepare-and-run-workflow workflow []" + step_with_val = "prepare-and-run-workflow name_of_workflow" + step_only_type = "await-qa" + + # Step of the form "step-type step-value [step_arg1, ...]" + step_with_val_and_args = CapabilityStep.from_str(step_with_val_and_args) + assert step_with_val_and_args.step_type is CapabilityStepType.PrepareAndRunWorkflow + assert step_with_val_and_args.step_value == "deliver" + assert step_with_val_and_args.step_args == [ + "-r", + "-l .", + "shared/workspaces/test/test_data/spool/724126739/17A-109.sb33151331.eb33786546.57892.65940042824", + ] + + # Step of the form "step-type step-value [step_arg]" + step_with_val_and_one_arg = CapabilityStep.from_str(step_with_val_and_one_arg) + assert ( + step_with_val_and_one_arg.step_type is CapabilityStepType.PrepareAndRunWorkflow + ) + assert step_with_val_and_one_arg.step_value == "null" + assert step_with_val_and_one_arg.step_args == ["-g"] + + # Step of the form "step-type step-value []" + step_with_val_and_empty_args = CapabilityStep.from_str(step_with_val_and_empty_args) + assert ( + step_with_val_and_empty_args.step_type + is CapabilityStepType.PrepareAndRunWorkflow + ) + assert step_with_val_and_empty_args.step_value == "workflow" + assert step_with_val_and_empty_args.step_args is None + + # Step of the form "step-type step-value" + step_with_val = CapabilityStep.from_str(step_with_val) + assert step_with_val.step_type is CapabilityStepType.PrepareAndRunWorkflow + assert step_with_val.step_value == "name_of_workflow" + assert step_with_val.step_args is None + + # Step of the form "step-type" + step_only_type = CapabilityStep.from_str(step_only_type) + assert step_only_type.step_type is CapabilityStepType.AwaitQA + assert step_only_type.step_value is None + assert step_only_type.step_args is None + + +def test_capability_step_from_str_error(): + """ + Tests that an invalid capability step strings will correctly throw an error + """ + step_empty = "" + step_bad_format = "!step.type step$value arg1 arg2 arg3" + step_invalid_type = "invalid-step-type step-value [arg1, arg2]" + + with pytest.raises(MalformedCapabilityStep): + CapabilityStep.from_str(step_empty) + CapabilityStep.from_str(step_bad_format) + CapabilityStep.from_str(step_invalid_type)