aiida-pythonjob 0.3.3__tar.gz → 0.3.5__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.
- {aiida_pythonjob-0.3.3/src/aiida_pythonjob.egg-info → aiida_pythonjob-0.3.5}/PKG-INFO +2 -2
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/README.md +1 -1
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/pyproject.toml +7 -4
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/__init__.py +7 -6
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/calculations/__init__.py +1 -1
- aiida_pythonjob-0.3.5/src/aiida_pythonjob/calculations/common.py +121 -0
- aiida_pythonjob-0.3.5/src/aiida_pythonjob/calculations/pyfunction.py +133 -0
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/calculations/pythonjob.py +122 -156
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/data/__init__.py +1 -1
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/data/deserializer.py +20 -10
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/data/jsonable_data.py +9 -1
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/data/pickled_data.py +1 -1
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/data/serializer.py +11 -23
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/decorator.py +2 -0
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/launch.py +43 -23
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/parsers/pythonjob.py +4 -7
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/parsers/utils.py +12 -6
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/utils.py +6 -5
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5/src/aiida_pythonjob.egg-info}/PKG-INFO +2 -2
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob.egg-info/SOURCES.txt +3 -2
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob.egg-info/entry_points.txt +3 -3
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/tests/test_data.py +10 -18
- aiida_pythonjob-0.3.5/tests/test_jsonable_data.py +135 -0
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/tests/test_parser.py +35 -32
- aiida_pythonjob-0.3.5/tests/test_pickled_data.py +95 -0
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/tests/test_pyfunction.py +31 -7
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/tests/test_pythonjob.py +13 -1
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/tests/test_serializer.py +9 -7
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/tests/test_utils.py +1 -0
- aiida_pythonjob-0.3.3/src/aiida_pythonjob/calculations/pyfunction.py +0 -211
- aiida_pythonjob-0.3.3/src/aiida_pythonjob/data/data_wrapper.py +0 -19
- aiida_pythonjob-0.3.3/src/aiida_pythonjob/ports_adapter.py +0 -106
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/LICENSE +0 -0
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/setup.cfg +0 -0
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/calculations/utils.py +0 -0
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/config.py +0 -0
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/data/atoms.py +0 -0
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/data/common_data.py +0 -0
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/data/utils.py +0 -0
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/parsers/__init__.py +0 -0
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob.egg-info/dependency_links.txt +0 -0
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob.egg-info/requires.txt +0 -0
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob.egg-info/top_level.txt +0 -0
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/tests/test_create_env.py +0 -0
- {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/tests/test_entry_points.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiida-pythonjob
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.5
|
|
4
4
|
Summary: Run Python functions on a remote computer.
|
|
5
5
|
Author-email: Xing Wang <xingwang1991@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -61,7 +61,7 @@ Dynamic: license-file
|
|
|
61
61
|
|
|
62
62
|
# AiiDA-PythonJob
|
|
63
63
|
[](https://badge.fury.io/py/aiida-pythonjob)
|
|
64
|
-
[](https://github.com/aiidateam/aiida-pythonjob/actions/workflows/ci.yml)
|
|
64
|
+
[](https://github.com/aiidateam/aiida-pythonjob/actions/workflows/ci-tests.yml)
|
|
65
65
|
[](https://codecov.io/gh/aiidateam/aiida-pythonjob)
|
|
66
66
|
[](http://aiida-pythonjob.readthedocs.io/)
|
|
67
67
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# AiiDA-PythonJob
|
|
2
2
|
[](https://badge.fury.io/py/aiida-pythonjob)
|
|
3
|
-
[](https://github.com/aiidateam/aiida-pythonjob/actions/workflows/ci.yml)
|
|
3
|
+
[](https://github.com/aiidateam/aiida-pythonjob/actions/workflows/ci-tests.yml)
|
|
4
4
|
[](https://codecov.io/gh/aiidateam/aiida-pythonjob)
|
|
5
5
|
[](http://aiida-pythonjob.readthedocs.io/)
|
|
6
6
|
|
|
@@ -60,13 +60,13 @@ Source = "https://github.com/aiidateam/aiida-pythonjob"
|
|
|
60
60
|
"pythonjob.builtins.float" = "aiida.orm.nodes.data.float:Float"
|
|
61
61
|
"pythonjob.builtins.str" = "aiida.orm.nodes.data.str:Str"
|
|
62
62
|
"pythonjob.builtins.bool" = "aiida.orm.nodes.data.bool:Bool"
|
|
63
|
-
"pythonjob.builtins.list" = "
|
|
64
|
-
"pythonjob.builtins.dict" = "
|
|
63
|
+
"pythonjob.builtins.list" = "aiida.orm.nodes.data.list:List"
|
|
64
|
+
"pythonjob.builtins.dict" = "aiida.orm.nodes.data.dict:Dict"
|
|
65
65
|
"pythonjob.numpy.float32" = "aiida.orm.nodes.data.float:Float"
|
|
66
66
|
"pythonjob.numpy.float64" = "aiida.orm.nodes.data.float:Float"
|
|
67
67
|
"pythonjob.numpy.int64" = "aiida.orm.nodes.data.int:Int"
|
|
68
68
|
"pythonjob.numpy.bool_" = "aiida.orm.nodes.data.bool:Bool"
|
|
69
|
-
"pythonjob.numpy.ndarray" = "
|
|
69
|
+
"pythonjob.numpy.ndarray" = "aiida.orm.nodes.data.array.array.ArrayData"
|
|
70
70
|
|
|
71
71
|
[project.entry-points."aiida.calculations"]
|
|
72
72
|
"pythonjob.pythonjob" = "aiida_pythonjob.calculations.pythonjob:PythonJob"
|
|
@@ -98,12 +98,15 @@ filterwarnings = [
|
|
|
98
98
|
source = ["src/aiida_pythonjob"]
|
|
99
99
|
|
|
100
100
|
[tool.ruff]
|
|
101
|
-
line-length =
|
|
101
|
+
line-length = 121
|
|
102
102
|
|
|
103
103
|
[tool.ruff.lint]
|
|
104
104
|
ignore = [
|
|
105
105
|
"F403",
|
|
106
106
|
"F405",
|
|
107
|
+
"F821",
|
|
108
|
+
"F722",
|
|
109
|
+
"E203",
|
|
107
110
|
"PLR0911",
|
|
108
111
|
"PLR0912",
|
|
109
112
|
"PLR0913",
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
"""AiiDA plugin that run Python function on remote computers."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.3.
|
|
3
|
+
__version__ = "0.3.5"
|
|
4
4
|
|
|
5
5
|
from node_graph import socket_spec as spec
|
|
6
6
|
|
|
7
7
|
from .calculations import PyFunction, PythonJob
|
|
8
8
|
from .decorator import pyfunction
|
|
9
|
-
from .launch import prepare_pythonjob_inputs
|
|
9
|
+
from .launch import prepare_pyfunction_inputs, prepare_pythonjob_inputs
|
|
10
10
|
from .parsers import PythonJobParser
|
|
11
11
|
|
|
12
12
|
__all__ = (
|
|
13
|
-
"PythonJob",
|
|
14
|
-
"PyFunction",
|
|
15
|
-
"pyfunction",
|
|
16
13
|
"PickledData",
|
|
17
|
-
"
|
|
14
|
+
"PyFunction",
|
|
15
|
+
"PythonJob",
|
|
18
16
|
"PythonJobParser",
|
|
17
|
+
"prepare_pyfunction_inputs",
|
|
18
|
+
"prepare_pythonjob_inputs",
|
|
19
|
+
"pyfunction",
|
|
19
20
|
"spec",
|
|
20
21
|
)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Common helpers and mixins shared by PyFunction and PythonJob."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, MutableMapping
|
|
6
|
+
|
|
7
|
+
from aiida.orm import Data, Str, to_aiida_type
|
|
8
|
+
|
|
9
|
+
from aiida_pythonjob.data.deserializer import deserialize_to_raw_python_data
|
|
10
|
+
|
|
11
|
+
# Attribute keys stored on ProcessNode.base.attributes
|
|
12
|
+
ATTR_OUTPUTS_SPEC = "outputs_spec"
|
|
13
|
+
ATTR_USE_PICKLE = "use_pickle"
|
|
14
|
+
ATTR_SERIALIZERS = "serializers"
|
|
15
|
+
ATTR_DESERIALIZERS = "deserializers"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def add_common_function_io(spec) -> None:
|
|
19
|
+
"""Attach inputs common to both in-process and remote Python execution.
|
|
20
|
+
|
|
21
|
+
Works with both :class:`~aiida.engine.ProcessSpec` and
|
|
22
|
+
:class:`~aiida.engine.CalcJobProcessSpec`.
|
|
23
|
+
"""
|
|
24
|
+
spec.input_namespace("function_data", dynamic=True, required=True)
|
|
25
|
+
spec.input(
|
|
26
|
+
"metadata.outputs_spec",
|
|
27
|
+
valid_type=dict,
|
|
28
|
+
required=False,
|
|
29
|
+
help="Specification for the outputs.",
|
|
30
|
+
)
|
|
31
|
+
spec.input(
|
|
32
|
+
"metadata.use_pickle",
|
|
33
|
+
valid_type=bool,
|
|
34
|
+
required=False,
|
|
35
|
+
help="Allow pickling of function inputs and outputs.",
|
|
36
|
+
)
|
|
37
|
+
spec.input("process_label", valid_type=Str, serializer=to_aiida_type, required=False)
|
|
38
|
+
|
|
39
|
+
spec.input_namespace("function_inputs", valid_type=Data, required=False)
|
|
40
|
+
|
|
41
|
+
spec.input(
|
|
42
|
+
"metadata.deserializers",
|
|
43
|
+
valid_type=dict,
|
|
44
|
+
required=False,
|
|
45
|
+
help="Deserializers to convert input AiiDA nodes to raw Python data.",
|
|
46
|
+
)
|
|
47
|
+
spec.input(
|
|
48
|
+
"metadata.serializers",
|
|
49
|
+
valid_type=dict,
|
|
50
|
+
required=False,
|
|
51
|
+
help="Serializers to convert raw Python data to AiiDA nodes.",
|
|
52
|
+
)
|
|
53
|
+
spec.exit_code(
|
|
54
|
+
320,
|
|
55
|
+
"ERROR_DESERIALIZE_INPUTS_FAILED",
|
|
56
|
+
invalidates_cache=True,
|
|
57
|
+
message="Failed to unpickle inputs.\n{exception}\n{traceback}",
|
|
58
|
+
)
|
|
59
|
+
spec.exit_code(
|
|
60
|
+
321,
|
|
61
|
+
"ERROR_INVALID_OUTPUT",
|
|
62
|
+
invalidates_cache=True,
|
|
63
|
+
message="The output file contains invalid output.",
|
|
64
|
+
)
|
|
65
|
+
spec.exit_code(
|
|
66
|
+
322,
|
|
67
|
+
"ERROR_RESULT_OUTPUT_MISMATCH",
|
|
68
|
+
invalidates_cache=True,
|
|
69
|
+
message="The number of results does not match the number of outputs.",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
spec.inputs.validator = validate_function_inputs
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def validate_function_inputs(inputs: MutableMapping[str, Any], _):
|
|
76
|
+
"""Validate that ``function_inputs`` can be deserialized.
|
|
77
|
+
|
|
78
|
+
Uses ``metadata.deserializers`` if provided. Raises if invalid.
|
|
79
|
+
"""
|
|
80
|
+
deserializers = inputs.get("metadata", {}).get("deserializers", {})
|
|
81
|
+
function_inputs = inputs.get("function_inputs", {})
|
|
82
|
+
# This should raise if any datum cannot be deserialized.
|
|
83
|
+
deserialize_to_raw_python_data(function_inputs, deserializers=deserializers, dry_run=True)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class FunctionProcessMixin:
|
|
87
|
+
"""Mixin providing common metadata handling and labeling logic.
|
|
88
|
+
|
|
89
|
+
Place this mixin **before** :class:`~aiida.engine.Process`/``CalcJob`` in the MRO.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
label_template: str = "{name}"
|
|
93
|
+
default_name: str = "anonymous_function"
|
|
94
|
+
|
|
95
|
+
def _extract_declared_name(self) -> str | None: # pragma: no cover - trivial
|
|
96
|
+
"""Try to read a user-declared function name from inputs.
|
|
97
|
+
|
|
98
|
+
Subclasses may extend this (e.g. by inspecting a pickled function).
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
if "name" in self.inputs.function_data:
|
|
102
|
+
return self.inputs.function_data.name
|
|
103
|
+
except Exception:
|
|
104
|
+
pass
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
def get_function_name(self) -> str: # used by both PyFunction and PythonJob
|
|
108
|
+
return self._extract_declared_name() or self.default_name
|
|
109
|
+
|
|
110
|
+
def _build_process_label(self) -> str: # called by AiiDA engine
|
|
111
|
+
if "process_label" in self.inputs:
|
|
112
|
+
return self.inputs.process_label.value
|
|
113
|
+
return self.label_template.format(name=self.get_function_name())
|
|
114
|
+
|
|
115
|
+
def _setup_metadata(self, metadata: dict) -> None: # type: ignore[override]
|
|
116
|
+
"""Store common metadata on the ProcessNode and forward the rest."""
|
|
117
|
+
self.node.base.attributes.set(ATTR_OUTPUTS_SPEC, metadata.pop("outputs_spec", {}))
|
|
118
|
+
self.node.base.attributes.set(ATTR_USE_PICKLE, metadata.pop("use_pickle", False))
|
|
119
|
+
self.node.base.attributes.set(ATTR_SERIALIZERS, metadata.pop("serializers", {}))
|
|
120
|
+
self.node.base.attributes.set(ATTR_DESERIALIZERS, metadata.pop("deserializers", {}))
|
|
121
|
+
super()._setup_metadata(metadata)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Proess to run a Python function locally"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import traceback
|
|
6
|
+
import typing as t
|
|
7
|
+
|
|
8
|
+
import cloudpickle
|
|
9
|
+
import plumpy
|
|
10
|
+
from aiida.common.lang import override
|
|
11
|
+
from aiida.engine import Process, ProcessSpec
|
|
12
|
+
from aiida.engine.processes.exit_code import ExitCode
|
|
13
|
+
from aiida.orm import CalcFunctionNode
|
|
14
|
+
from node_graph.socket_spec import SocketSpec
|
|
15
|
+
|
|
16
|
+
from aiida_pythonjob.calculations.common import (
|
|
17
|
+
ATTR_DESERIALIZERS,
|
|
18
|
+
ATTR_OUTPUTS_SPEC,
|
|
19
|
+
ATTR_SERIALIZERS,
|
|
20
|
+
ATTR_USE_PICKLE,
|
|
21
|
+
FunctionProcessMixin,
|
|
22
|
+
add_common_function_io,
|
|
23
|
+
)
|
|
24
|
+
from aiida_pythonjob.data.deserializer import deserialize_to_raw_python_data
|
|
25
|
+
from aiida_pythonjob.parsers.utils import parse_outputs
|
|
26
|
+
|
|
27
|
+
__all__ = ("PyFunction",)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PyFunction(FunctionProcessMixin, Process):
|
|
31
|
+
"""Run a Python function in-process, using :class:`SocketSpec` for I/O."""
|
|
32
|
+
|
|
33
|
+
_node_class = CalcFunctionNode
|
|
34
|
+
label_template = "{name}"
|
|
35
|
+
default_name = "anonymous_function"
|
|
36
|
+
|
|
37
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
38
|
+
if kwargs.get("enable_persistence", False):
|
|
39
|
+
raise RuntimeError("Cannot persist a function process")
|
|
40
|
+
super().__init__(enable_persistence=False, *args, **kwargs) # type: ignore[misc]
|
|
41
|
+
self._func = None
|
|
42
|
+
|
|
43
|
+
@override
|
|
44
|
+
def load_instance_state(
|
|
45
|
+
self, saved_state: t.MutableMapping[str, t.Any], load_context: plumpy.persistence.LoadSaveContext
|
|
46
|
+
) -> None:
|
|
47
|
+
"""Load the instance state (restore pickled function)."""
|
|
48
|
+
super().load_instance_state(saved_state, load_context)
|
|
49
|
+
self._func = cloudpickle.loads(self.inputs.function_data.pickled_function)
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def func(self) -> t.Callable[..., t.Any]:
|
|
53
|
+
if self._func is None:
|
|
54
|
+
self._func = cloudpickle.loads(self.inputs.function_data.pickled_function)
|
|
55
|
+
return self._func
|
|
56
|
+
|
|
57
|
+
def _extract_declared_name(self) -> str | None:
|
|
58
|
+
name = super()._extract_declared_name()
|
|
59
|
+
if name:
|
|
60
|
+
return name
|
|
61
|
+
try:
|
|
62
|
+
return self.func.__name__
|
|
63
|
+
except Exception:
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def define(cls, spec: ProcessSpec) -> None: # type: ignore[override]
|
|
68
|
+
"""Define inputs/outputs and exit codes."""
|
|
69
|
+
super().define(spec)
|
|
70
|
+
add_common_function_io(spec)
|
|
71
|
+
spec.inputs.dynamic = True
|
|
72
|
+
spec.outputs.dynamic = True
|
|
73
|
+
spec.exit_code(
|
|
74
|
+
323,
|
|
75
|
+
"ERROR_FUNCTION_EXECUTION_FAILED",
|
|
76
|
+
invalidates_cache=True,
|
|
77
|
+
message="Function execution failed.\n{exception}\n{traceback}",
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
@override
|
|
81
|
+
def _setup_db_record(self) -> None:
|
|
82
|
+
super()._setup_db_record()
|
|
83
|
+
self.node.store_source_info(self.func)
|
|
84
|
+
|
|
85
|
+
def execute(self) -> dict[str, t.Any] | None:
|
|
86
|
+
"""Mirror calcfunction behavior: unwrap single-output dicts to a bare value."""
|
|
87
|
+
result = super().execute()
|
|
88
|
+
if result and len(result) == 1 and self.SINGLE_OUTPUT_LINKNAME in result:
|
|
89
|
+
return result[self.SINGLE_OUTPUT_LINKNAME]
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
@override
|
|
93
|
+
def run(self) -> ExitCode | None:
|
|
94
|
+
# Respect caching semantics (from aiida-core calcfunction implementation)
|
|
95
|
+
if self.node.exit_status is not None:
|
|
96
|
+
return ExitCode(self.node.exit_status, self.node.exit_message)
|
|
97
|
+
|
|
98
|
+
# Deserialize inputs
|
|
99
|
+
try:
|
|
100
|
+
inputs = dict(self.inputs.function_inputs or {})
|
|
101
|
+
deserializers = self.node.base.attributes.get(ATTR_DESERIALIZERS, {})
|
|
102
|
+
inputs = deserialize_to_raw_python_data(inputs, deserializers=deserializers)
|
|
103
|
+
except Exception as exception:
|
|
104
|
+
return self.exit_codes.ERROR_DESERIALIZE_INPUTS_FAILED.format(
|
|
105
|
+
exception=str(exception), traceback=traceback.format_exc()
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Execute function
|
|
109
|
+
try:
|
|
110
|
+
results = self.func(**inputs)
|
|
111
|
+
except Exception as exception:
|
|
112
|
+
return self.exit_codes.ERROR_FUNCTION_EXECUTION_FAILED.format(
|
|
113
|
+
exception=str(exception), traceback=traceback.format_exc()
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Parse & attach outputs
|
|
117
|
+
outputs_spec = SocketSpec.from_dict(self.node.base.attributes.get(ATTR_OUTPUTS_SPEC) or {})
|
|
118
|
+
use_pickle = self.node.base.attributes.get(ATTR_USE_PICKLE, False)
|
|
119
|
+
serializers = self.node.base.attributes.get(ATTR_SERIALIZERS, {})
|
|
120
|
+
outputs, exit_code = parse_outputs(
|
|
121
|
+
results,
|
|
122
|
+
output_spec=outputs_spec,
|
|
123
|
+
exit_codes=self.exit_codes,
|
|
124
|
+
logger=self.logger,
|
|
125
|
+
serializers=serializers,
|
|
126
|
+
use_pickle=use_pickle,
|
|
127
|
+
)
|
|
128
|
+
if exit_code:
|
|
129
|
+
return exit_code
|
|
130
|
+
|
|
131
|
+
for name, value in (outputs or {}).items():
|
|
132
|
+
self.out(name, value)
|
|
133
|
+
return ExitCode()
|