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.
Files changed (45) hide show
  1. {aiida_pythonjob-0.3.3/src/aiida_pythonjob.egg-info → aiida_pythonjob-0.3.5}/PKG-INFO +2 -2
  2. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/README.md +1 -1
  3. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/pyproject.toml +7 -4
  4. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/__init__.py +7 -6
  5. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/calculations/__init__.py +1 -1
  6. aiida_pythonjob-0.3.5/src/aiida_pythonjob/calculations/common.py +121 -0
  7. aiida_pythonjob-0.3.5/src/aiida_pythonjob/calculations/pyfunction.py +133 -0
  8. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/calculations/pythonjob.py +122 -156
  9. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/data/__init__.py +1 -1
  10. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/data/deserializer.py +20 -10
  11. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/data/jsonable_data.py +9 -1
  12. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/data/pickled_data.py +1 -1
  13. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/data/serializer.py +11 -23
  14. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/decorator.py +2 -0
  15. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/launch.py +43 -23
  16. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/parsers/pythonjob.py +4 -7
  17. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/parsers/utils.py +12 -6
  18. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/utils.py +6 -5
  19. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5/src/aiida_pythonjob.egg-info}/PKG-INFO +2 -2
  20. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob.egg-info/SOURCES.txt +3 -2
  21. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob.egg-info/entry_points.txt +3 -3
  22. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/tests/test_data.py +10 -18
  23. aiida_pythonjob-0.3.5/tests/test_jsonable_data.py +135 -0
  24. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/tests/test_parser.py +35 -32
  25. aiida_pythonjob-0.3.5/tests/test_pickled_data.py +95 -0
  26. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/tests/test_pyfunction.py +31 -7
  27. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/tests/test_pythonjob.py +13 -1
  28. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/tests/test_serializer.py +9 -7
  29. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/tests/test_utils.py +1 -0
  30. aiida_pythonjob-0.3.3/src/aiida_pythonjob/calculations/pyfunction.py +0 -211
  31. aiida_pythonjob-0.3.3/src/aiida_pythonjob/data/data_wrapper.py +0 -19
  32. aiida_pythonjob-0.3.3/src/aiida_pythonjob/ports_adapter.py +0 -106
  33. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/LICENSE +0 -0
  34. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/setup.cfg +0 -0
  35. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/calculations/utils.py +0 -0
  36. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/config.py +0 -0
  37. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/data/atoms.py +0 -0
  38. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/data/common_data.py +0 -0
  39. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/data/utils.py +0 -0
  40. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob/parsers/__init__.py +0 -0
  41. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob.egg-info/dependency_links.txt +0 -0
  42. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob.egg-info/requires.txt +0 -0
  43. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/src/aiida_pythonjob.egg-info/top_level.txt +0 -0
  44. {aiida_pythonjob-0.3.3 → aiida_pythonjob-0.3.5}/tests/test_create_env.py +0 -0
  45. {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
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
  [![PyPI version](https://badge.fury.io/py/aiida-pythonjob.svg)](https://badge.fury.io/py/aiida-pythonjob)
64
- [![Unit test](https://github.com/aiidateam/aiida-pythonjob/actions/workflows/ci.yml/badge.svg)](https://github.com/aiidateam/aiida-pythonjob/actions/workflows/ci.yml)
64
+ [![Unit test](https://github.com/aiidateam/aiida-pythonjob/actions/workflows/ci-tests.yml/badge.svg)](https://github.com/aiidateam/aiida-pythonjob/actions/workflows/ci-tests.yml)
65
65
  [![codecov](https://codecov.io/gh/aiidateam/aiida-pythonjob/branch/main/graph/badge.svg)](https://codecov.io/gh/aiidateam/aiida-pythonjob)
66
66
  [![Docs status](https://readthedocs.org/projects/aiida-pythonjob/badge)](http://aiida-pythonjob.readthedocs.io/)
67
67
 
@@ -1,6 +1,6 @@
1
1
  # AiiDA-PythonJob
2
2
  [![PyPI version](https://badge.fury.io/py/aiida-pythonjob.svg)](https://badge.fury.io/py/aiida-pythonjob)
3
- [![Unit test](https://github.com/aiidateam/aiida-pythonjob/actions/workflows/ci.yml/badge.svg)](https://github.com/aiidateam/aiida-pythonjob/actions/workflows/ci.yml)
3
+ [![Unit test](https://github.com/aiidateam/aiida-pythonjob/actions/workflows/ci-tests.yml/badge.svg)](https://github.com/aiidateam/aiida-pythonjob/actions/workflows/ci-tests.yml)
4
4
  [![codecov](https://codecov.io/gh/aiidateam/aiida-pythonjob/branch/main/graph/badge.svg)](https://codecov.io/gh/aiidateam/aiida-pythonjob)
5
5
  [![Docs status](https://readthedocs.org/projects/aiida-pythonjob/badge)](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" = "aiida_pythonjob.data.data_wrapper:List"
64
- "pythonjob.builtins.dict" = "aiida_pythonjob.data.data_wrapper: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" = "aiida_pythonjob.data.data_wrapper:ArrayData"
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 = 120
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"
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
- "prepare_pythonjob_inputs",
14
+ "PyFunction",
15
+ "PythonJob",
18
16
  "PythonJobParser",
17
+ "prepare_pyfunction_inputs",
18
+ "prepare_pythonjob_inputs",
19
+ "pyfunction",
19
20
  "spec",
20
21
  )
@@ -1,4 +1,4 @@
1
1
  from .pyfunction import PyFunction
2
2
  from .pythonjob import PythonJob
3
3
 
4
- __all__ = ("PythonJob", "PyFunction")
4
+ __all__ = ("PyFunction", "PythonJob")
@@ -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()