Skip to content

_mapper

WorkflowNodeMapper

Bases: Task

Add ayon-workflow on the top of taskflow task to validate signatures.

Source code in client/ayon_workflow/plugin_system/_mapper.py
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
class WorkflowNodeMapper(taskflow.task.Task):
    """ Add ayon-workflow on the top of taskflow task to validate signatures.
    """

    # The version of the node. Used to determine
    # compatibility with previously saved workflows.
    version: str = ""

    # The name of the node (inherited from class name)
    name: Optional[str] = None

    # The input(s) of the node.
    inputs: list[InputAttribute] = []

    # The output(s) of the node.
    outputs: list[OutputAttribute] = []

    def __init__(
        self,
        name: Optional[str] = None,
        provides: Optional[list[str]] = None,
        inject: Optional[dict[str, Any]] = None,
        rebind: Optional[dict[str, str]] = None,
    ):
        super().__init__(
            name=name,
            provides=provides,
            inject=inject,
            rebind=rebind,
        )
        self.pre_revert_tasks = []

    def revert(self, result, *args, **kwargs) -> list[Any]:
        """https://docs.openstack.org/taskflow/latest/user/atoms.html#taskflow.atom.Atom.revert
        """
        # Pre-revert mechanism, call execute function for
        # nodes connected to the 'revert' output slot.
        revert_results = []
        for pre_revert_task in self.pre_revert_tasks:
            rev_inputs = [
                key for key, value
                in pre_revert_task.rebind.items()
                if value == f"result.{self.name}"
            ]
            pre_kwargs = {
                rev_input: result
                for rev_input in rev_inputs
            }
            revert_results.append(
                pre_revert_task.execute(**pre_kwargs)
            )

            revert_exec = self.revert_execute(*args, **kwargs)
            revert_results.append(revert_exec)

        return revert_results

    @classmethod
    def to_workflow_node(cls) -> WorkflowNode:
        """ Converts the node class to a plugin description dictionary.
        """
        if not cls.version or cls.version.count(".") < 2:
            raise ValueError(
                f"Plugin version '{cls.version}' does not match format "
                "(e.g., '1.0.0')."
            )

        # Validate explicit inputs
        inputs = []
        input_names = []
        execute_insp = inspect.signature(cls.execute)
        for insp_data in cls.inputs:
            inp = insp_data.name
            if inp not in execute_insp.parameters:
                raise ValueError(
                    f"Input {inp} not found in execute signature"
                )

            edited_insp_data = copy.deepcopy(insp_data)
            edited_insp_data.type = execute_insp.parameters[inp].annotation
            edited_insp_data.default = execute_insp.parameters[inp].default

            input_names.append(inp)
            inputs.append(edited_insp_data)

        # Detect missing input
        for inp, inp_data in execute_insp.parameters.items():
            if inp == "self" or inp_data.kind in (
                inspect.Parameter.VAR_POSITIONAL,
                inspect.Parameter.VAR_KEYWORD,
            ):
                continue
            if (
                inp not in input_names
                and inp_data.default is inspect.Parameter.empty
            ):
                raise ValueError(
                    f"{inp} in execute signature with no "
                    "default value but not defined in node inputs"
                )

        # Validate outputs
        out_insp = inspect.signature(cls.execute).return_annotation
        if (
            out_insp is inspect.Signature.empty
            and len(cls.outputs) > 0
        ):
            raise TypeError(
                "Cannot determine output(s) return type "
                "from execute method"
            )

        # Multiple outputs
        out_origin = typing.get_origin(out_insp)
        out_args = typing.get_args(out_insp)

        if (
            not out_args
            and len(cls.outputs) > 1
        ):
            raise TypeError(
                "Multiple output defined, "
                "but only one is return annotation"
            )

        outputs = []
        for idx, out_data in enumerate(cls.outputs):
            edited_out_data = copy.deepcopy(out_data)
            out_type = typing.Any

            if len(cls.outputs) == 1:
                out_type = out_insp
            elif out_origin is tuple and idx < len(out_args):
                out_type = out_args[idx]
            elif out_origin is dict and len(out_args) == 2:
                out_type = out_args[1]

            edited_out_data.type = out_type
            outputs.append(edited_out_data)

        return WorkflowNode(
            name=cls.name or cls.__name__,
            version=cls.version,
            inputs=inputs,
            outputs=outputs,
            description=cls.__doc__ or "",
            implementation=cls,
        )

revert(result, *args, **kwargs)

https://docs.openstack.org/taskflow/latest/user/atoms.html#taskflow.atom.Atom.revert

Source code in client/ayon_workflow/plugin_system/_mapper.py
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
def revert(self, result, *args, **kwargs) -> list[Any]:
    """https://docs.openstack.org/taskflow/latest/user/atoms.html#taskflow.atom.Atom.revert
    """
    # Pre-revert mechanism, call execute function for
    # nodes connected to the 'revert' output slot.
    revert_results = []
    for pre_revert_task in self.pre_revert_tasks:
        rev_inputs = [
            key for key, value
            in pre_revert_task.rebind.items()
            if value == f"result.{self.name}"
        ]
        pre_kwargs = {
            rev_input: result
            for rev_input in rev_inputs
        }
        revert_results.append(
            pre_revert_task.execute(**pre_kwargs)
        )

        revert_exec = self.revert_execute(*args, **kwargs)
        revert_results.append(revert_exec)

    return revert_results

to_workflow_node() classmethod

Converts the node class to a plugin description dictionary.

Source code in client/ayon_workflow/plugin_system/_mapper.py
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
@classmethod
def to_workflow_node(cls) -> WorkflowNode:
    """ Converts the node class to a plugin description dictionary.
    """
    if not cls.version or cls.version.count(".") < 2:
        raise ValueError(
            f"Plugin version '{cls.version}' does not match format "
            "(e.g., '1.0.0')."
        )

    # Validate explicit inputs
    inputs = []
    input_names = []
    execute_insp = inspect.signature(cls.execute)
    for insp_data in cls.inputs:
        inp = insp_data.name
        if inp not in execute_insp.parameters:
            raise ValueError(
                f"Input {inp} not found in execute signature"
            )

        edited_insp_data = copy.deepcopy(insp_data)
        edited_insp_data.type = execute_insp.parameters[inp].annotation
        edited_insp_data.default = execute_insp.parameters[inp].default

        input_names.append(inp)
        inputs.append(edited_insp_data)

    # Detect missing input
    for inp, inp_data in execute_insp.parameters.items():
        if inp == "self" or inp_data.kind in (
            inspect.Parameter.VAR_POSITIONAL,
            inspect.Parameter.VAR_KEYWORD,
        ):
            continue
        if (
            inp not in input_names
            and inp_data.default is inspect.Parameter.empty
        ):
            raise ValueError(
                f"{inp} in execute signature with no "
                "default value but not defined in node inputs"
            )

    # Validate outputs
    out_insp = inspect.signature(cls.execute).return_annotation
    if (
        out_insp is inspect.Signature.empty
        and len(cls.outputs) > 0
    ):
        raise TypeError(
            "Cannot determine output(s) return type "
            "from execute method"
        )

    # Multiple outputs
    out_origin = typing.get_origin(out_insp)
    out_args = typing.get_args(out_insp)

    if (
        not out_args
        and len(cls.outputs) > 1
    ):
        raise TypeError(
            "Multiple output defined, "
            "but only one is return annotation"
        )

    outputs = []
    for idx, out_data in enumerate(cls.outputs):
        edited_out_data = copy.deepcopy(out_data)
        out_type = typing.Any

        if len(cls.outputs) == 1:
            out_type = out_insp
        elif out_origin is tuple and idx < len(out_args):
            out_type = out_args[idx]
        elif out_origin is dict and len(out_args) == 2:
            out_type = out_args[1]

        edited_out_data.type = out_type
        outputs.append(edited_out_data)

    return WorkflowNode(
        name=cls.name or cls.__name__,
        version=cls.version,
        inputs=inputs,
        outputs=outputs,
        description=cls.__doc__ or "",
        implementation=cls,
    )

WorkflowTaskNode

Bases: WorkflowNodeMapper

Base class for workflow node that executes a task.

Source code in client/ayon_workflow/plugin_system/_mapper.py
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
class WorkflowTaskNode(WorkflowNodeMapper):
    """ Base class for workflow node that executes a task.
    """
    # Put the description of the node as class docstring.

    # The version of the node. Used to determine
    # compatibility with previously saved workflows.
    version: str = ""

    # The name of the node (inherited from class name)
    name: Optional[str] = None

    # The input(s) of the node.
    inputs: list[InputAttribute] = []

    # The output(s) of the node.
    outputs: list[OutputAttribute] = []

    def execute(self, *args, **kwargs) -> Any:
        """ Define the execution logic of the node.
        """
        raise NotImplementedError("Must be implemented by subclass.")

    def revert_execute(self, *args, **kwargs) -> Any:
        """ Define the revert logic of the node.
        """
        pass

execute(*args, **kwargs)

Define the execution logic of the node.

Source code in client/ayon_workflow/plugin_system/_mapper.py
184
185
186
187
def execute(self, *args, **kwargs) -> Any:
    """ Define the execution logic of the node.
    """
    raise NotImplementedError("Must be implemented by subclass.")

revert_execute(*args, **kwargs)

Define the revert logic of the node.

Source code in client/ayon_workflow/plugin_system/_mapper.py
189
190
191
192
def revert_execute(self, *args, **kwargs) -> Any:
    """ Define the revert logic of the node.
    """
    pass