aiida-pythonjob 0.2.5__tar.gz → 0.3.0__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.2.5 → aiida_pythonjob-0.3.0}/.readthedocs.yml +1 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/PKG-INFO +2 -1
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/docs/environment.yml +1 -1
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/docs/gallery/autogen/pyfunction.py +8 -8
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/docs/gallery/autogen/pythonjob.py +46 -17
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/docs/requirements.txt +3 -1
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/pyproject.toml +4 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/__init__.py +4 -1
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/calculations/pyfunction.py +15 -4
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/decorator.py +6 -2
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/launch.py +33 -26
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/parsers/pythonjob.py +6 -3
- aiida_pythonjob-0.3.0/src/aiida_pythonjob/ports_adapter.py +106 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/utils.py +132 -186
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/tests/test_parser.py +16 -22
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/tests/test_pyfunction.py +72 -76
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/tests/test_pythonjob.py +11 -25
- aiida_pythonjob-0.2.5/examples/test_add.py +0 -15
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/.github/workflows/ci.yml +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/.github/workflows/python-publish.yml +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/.gitignore +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/.pre-commit-config.yaml +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/LICENSE +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/README.md +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/docs/Makefile +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/docs/gallery/autogen/GALLERY_HEADER.rst +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/docs/make.bat +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/docs/source/conf.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/docs/source/index.rst +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/docs/source/installation.rst +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/docs/source/tutorial/dft.ipynb +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/docs/source/tutorial/html/atomization_energy.html +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/docs/source/tutorial/html/pythonjob_eos_emt.html +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/docs/source/tutorial/index.rst +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/calculations/__init__.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/calculations/pythonjob.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/calculations/utils.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/config.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/data/__init__.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/data/atoms.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/data/data_wrapper.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/data/deserializer.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/data/jsonable_data.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/data/pickled_data.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/data/serializer.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/data/utils.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/parsers/__init__.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/tests/conftest.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/tests/inputs_folder/another_input.txt +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/tests/test_create_env.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/tests/test_data.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/tests/test_entry_points.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/tests/test_serializer.py +0 -0
- {aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aiida-pythonjob
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Run Python functions on a remote computer.
|
|
5
5
|
Project-URL: Source, https://github.com/aiidateam/aiida-pythonjob
|
|
6
6
|
Author-email: Xing Wang <xingwang1991@gmail.com>
|
|
@@ -37,6 +37,7 @@ Requires-Python: >=3.9
|
|
|
37
37
|
Requires-Dist: aiida-core<3,>=2.3
|
|
38
38
|
Requires-Dist: ase
|
|
39
39
|
Requires-Dist: cloudpickle
|
|
40
|
+
Requires-Dist: node-graph==0.2.22
|
|
40
41
|
Provides-Extra: dev
|
|
41
42
|
Requires-Dist: hatch; extra == 'dev'
|
|
42
43
|
Provides-Extra: docs
|
|
@@ -6,14 +6,14 @@ PyFunction
|
|
|
6
6
|
|
|
7
7
|
######################################################################
|
|
8
8
|
# Default outputs
|
|
9
|
-
#
|
|
9
|
+
# -----------------
|
|
10
10
|
#
|
|
11
11
|
# The default output of the function is `result`. The `pyfunction` task
|
|
12
12
|
# will store the result as one node in the database with the key `result`.
|
|
13
13
|
#
|
|
14
14
|
from aiida import load_profile
|
|
15
15
|
from aiida.engine import run_get_node
|
|
16
|
-
from aiida_pythonjob import pyfunction
|
|
16
|
+
from aiida_pythonjob import pyfunction, spec
|
|
17
17
|
|
|
18
18
|
load_profile()
|
|
19
19
|
|
|
@@ -35,7 +35,7 @@ print("result: ", result)
|
|
|
35
35
|
#
|
|
36
36
|
|
|
37
37
|
|
|
38
|
-
@pyfunction(outputs=
|
|
38
|
+
@pyfunction(outputs=spec.namespace(sum=any, diff=any))
|
|
39
39
|
def add(x, y):
|
|
40
40
|
return {"sum": x + y, "diff": x - y}
|
|
41
41
|
|
|
@@ -48,7 +48,7 @@ print("diff: ", result["diff"])
|
|
|
48
48
|
|
|
49
49
|
######################################################################
|
|
50
50
|
# Namespace Output
|
|
51
|
-
#
|
|
51
|
+
# -----------------
|
|
52
52
|
#
|
|
53
53
|
# The `pyfunction` allows users to define namespace outputs. A namespace output
|
|
54
54
|
# is a dictionary with keys and values returned by a function. Each value in
|
|
@@ -70,7 +70,7 @@ from ase import Atoms # noqa: E402
|
|
|
70
70
|
from ase.build import bulk # noqa: E402
|
|
71
71
|
|
|
72
72
|
|
|
73
|
-
@pyfunction(outputs=
|
|
73
|
+
@pyfunction(outputs=spec.dynamic(Atoms))
|
|
74
74
|
def generate_structures(structure: Atoms, factor_lst: list) -> dict:
|
|
75
75
|
"""Scale the structure by the given factor_lst."""
|
|
76
76
|
scaled_structures = {}
|
|
@@ -78,12 +78,12 @@ def generate_structures(structure: Atoms, factor_lst: list) -> dict:
|
|
|
78
78
|
atoms = structure.copy()
|
|
79
79
|
atoms.set_cell(atoms.cell * factor_lst[i], scale_atoms=True)
|
|
80
80
|
scaled_structures[f"s_{i}"] = atoms
|
|
81
|
-
return
|
|
81
|
+
return scaled_structures
|
|
82
82
|
|
|
83
83
|
|
|
84
84
|
result, node = run_get_node(generate_structures, structure=bulk("Al"), factor_lst=[0.95, 1.0, 1.05])
|
|
85
85
|
print("scaled_structures: ")
|
|
86
|
-
for key, value in result
|
|
86
|
+
for key, value in result.items():
|
|
87
87
|
print(key, value)
|
|
88
88
|
|
|
89
89
|
|
|
@@ -115,7 +115,7 @@ print("exit_message:", node.exit_message)
|
|
|
115
115
|
|
|
116
116
|
######################################################################
|
|
117
117
|
# Define your data serializer and deserializer
|
|
118
|
-
#
|
|
118
|
+
# ----------------------------------------------
|
|
119
119
|
#
|
|
120
120
|
# PythonJob search data serializer from the `aiida.data` entry point by the
|
|
121
121
|
# module name and class name (e.g., `ase.atoms.Atoms`).
|
|
@@ -63,14 +63,14 @@ PythonJob
|
|
|
63
63
|
|
|
64
64
|
######################################################################
|
|
65
65
|
# Default outputs
|
|
66
|
-
#
|
|
66
|
+
# ----------------
|
|
67
67
|
#
|
|
68
68
|
# The default output of the function is `result`. The `PythonJob` task
|
|
69
69
|
# will store the result as one node in the database with the key `result`.
|
|
70
70
|
#
|
|
71
71
|
from aiida import load_profile
|
|
72
72
|
from aiida.engine import run_get_node
|
|
73
|
-
from aiida_pythonjob import PythonJob, prepare_pythonjob_inputs
|
|
73
|
+
from aiida_pythonjob import PythonJob, prepare_pythonjob_inputs, spec
|
|
74
74
|
|
|
75
75
|
load_profile()
|
|
76
76
|
|
|
@@ -91,7 +91,7 @@ print("result: ", result["result"])
|
|
|
91
91
|
# Custom outputs
|
|
92
92
|
# --------------
|
|
93
93
|
# If the function return a dictionary with fixed number of keys, and you
|
|
94
|
-
# want to store the values as separate outputs, you can specify the `
|
|
94
|
+
# want to store the values as separate outputs, you can specify the `outputs_spec` parameter.
|
|
95
95
|
# For a dynamic number of outputs, you can use the namespace output, which is explained later.
|
|
96
96
|
#
|
|
97
97
|
|
|
@@ -103,10 +103,7 @@ def add(x, y):
|
|
|
103
103
|
inputs = prepare_pythonjob_inputs(
|
|
104
104
|
add,
|
|
105
105
|
function_inputs={"x": 1, "y": 2},
|
|
106
|
-
|
|
107
|
-
{"name": "sum"},
|
|
108
|
-
{"name": "diff"},
|
|
109
|
-
],
|
|
106
|
+
outputs_spec=spec.namespace(sum=any, diff=any),
|
|
110
107
|
)
|
|
111
108
|
result, node = run_get_node(PythonJob, **inputs)
|
|
112
109
|
|
|
@@ -117,7 +114,7 @@ print("diff: ", result["diff"])
|
|
|
117
114
|
|
|
118
115
|
######################################################################
|
|
119
116
|
# Using parent folder
|
|
120
|
-
#
|
|
117
|
+
# -----------------------
|
|
121
118
|
# The parent_folder parameter allows a task to access the output files of
|
|
122
119
|
# a parent task. This feature is particularly useful when you want to reuse
|
|
123
120
|
# data generated by a previous computation in subsequent computations. In
|
|
@@ -142,7 +139,6 @@ def multiply(x, y):
|
|
|
142
139
|
inputs1 = prepare_pythonjob_inputs(
|
|
143
140
|
add,
|
|
144
141
|
function_inputs={"x": 1, "y": 2},
|
|
145
|
-
output_ports=[{"name": "sum"}],
|
|
146
142
|
)
|
|
147
143
|
|
|
148
144
|
result1, node1 = run_get_node(PythonJob, inputs=inputs1)
|
|
@@ -150,7 +146,6 @@ result1, node1 = run_get_node(PythonJob, inputs=inputs1)
|
|
|
150
146
|
inputs2 = prepare_pythonjob_inputs(
|
|
151
147
|
multiply,
|
|
152
148
|
function_inputs={"x": 1, "y": 2},
|
|
153
|
-
output_ports=[{"name": "product"}],
|
|
154
149
|
parent_folder=result1["remote_folder"],
|
|
155
150
|
)
|
|
156
151
|
|
|
@@ -160,7 +155,7 @@ print("result: ", result2)
|
|
|
160
155
|
|
|
161
156
|
######################################################################
|
|
162
157
|
# Upload files or folders to the remote computer
|
|
163
|
-
#
|
|
158
|
+
# -------------------------------------------------
|
|
164
159
|
# The `upload_files` parameter allows users to upload files or folders to
|
|
165
160
|
# the remote computer. The files will be uploaded to the working directory of the remote computer.
|
|
166
161
|
#
|
|
@@ -202,7 +197,7 @@ print("result: ", result["result"])
|
|
|
202
197
|
|
|
203
198
|
######################################################################
|
|
204
199
|
# Retrieve additional files from the remote computer
|
|
205
|
-
#
|
|
200
|
+
# ----------------------------------------------------
|
|
206
201
|
# Sometimes, one may want to retrieve additional files from the remote
|
|
207
202
|
# computer after the job has finished. For example, one may want to retrieve
|
|
208
203
|
# the output files generated by the `pw.x` calculation in Quantum ESPRESSO.
|
|
@@ -235,7 +230,7 @@ print("retrieved files: ", result["retrieved"].list_object_names())
|
|
|
235
230
|
|
|
236
231
|
######################################################################
|
|
237
232
|
# Namespace Output
|
|
238
|
-
#
|
|
233
|
+
# ------------------
|
|
239
234
|
#
|
|
240
235
|
# The `PythonJob` allows users to define namespace outputs. A namespace output
|
|
241
236
|
# is a dictionary with keys and values returned by a function. Each value in
|
|
@@ -264,21 +259,55 @@ def generate_structures(structure: Atoms, factor_lst: list) -> dict:
|
|
|
264
259
|
atoms = structure.copy()
|
|
265
260
|
atoms.set_cell(atoms.cell * factor_lst[i], scale_atoms=True)
|
|
266
261
|
scaled_structures[f"s_{i}"] = atoms
|
|
267
|
-
return
|
|
262
|
+
return scaled_structures
|
|
268
263
|
|
|
269
264
|
|
|
270
265
|
inputs = prepare_pythonjob_inputs(
|
|
271
266
|
generate_structures,
|
|
272
267
|
function_inputs={"structure": bulk("Al"), "factor_lst": [0.95, 1.0, 1.05]},
|
|
273
|
-
|
|
268
|
+
outputs_spec=spec.dynamic(Atoms),
|
|
274
269
|
)
|
|
275
270
|
|
|
276
271
|
result, node = run_get_node(PythonJob, inputs=inputs)
|
|
277
272
|
print("scaled_structures: ")
|
|
278
|
-
for key, value in result
|
|
273
|
+
for key, value in result.items():
|
|
279
274
|
print(key, value)
|
|
280
275
|
|
|
281
276
|
|
|
277
|
+
######################################################################
|
|
278
|
+
# --------------------------
|
|
279
|
+
# Nested Namespace
|
|
280
|
+
# --------------------------
|
|
281
|
+
#
|
|
282
|
+
# One can also define nested namespace outputs by specifying the "ports" parameter.
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def generate_structures(structure: Atoms, factor_lst: list) -> dict:
|
|
286
|
+
"""Scale the structure by the given factor_lst."""
|
|
287
|
+
scaled_structures = {}
|
|
288
|
+
volumes = {}
|
|
289
|
+
for i in range(len(factor_lst)):
|
|
290
|
+
atoms = structure.copy()
|
|
291
|
+
atoms.set_cell(atoms.cell * factor_lst[i], scale_atoms=True)
|
|
292
|
+
scaled_structures[f"s_{i}"] = atoms
|
|
293
|
+
volumes[f"v_{i}"] = atoms.get_volume()
|
|
294
|
+
return {
|
|
295
|
+
"scaled_structures": scaled_structures,
|
|
296
|
+
"volume": volumes,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
inputs = prepare_pythonjob_inputs(
|
|
301
|
+
generate_structures,
|
|
302
|
+
function_inputs={"structure": bulk("Al"), "factor_lst": [0.95, 1.0, 1.05]},
|
|
303
|
+
outputs_spec=spec.namespace(scaled_structures=spec.dynamic(Atoms), volume=spec.dynamic(float)),
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
result, node = run_get_node(PythonJob, inputs=inputs)
|
|
307
|
+
print("result: ", result["scaled_structures"])
|
|
308
|
+
print("volumes: ", result["volume"])
|
|
309
|
+
|
|
310
|
+
|
|
282
311
|
######################################################################
|
|
283
312
|
# What if my calculation fails?
|
|
284
313
|
# --------------------------------
|
|
@@ -375,7 +404,7 @@ print("exit_message:", node.exit_message)
|
|
|
375
404
|
|
|
376
405
|
######################################################################
|
|
377
406
|
# Define your data serializer and deserializer
|
|
378
|
-
#
|
|
407
|
+
# ----------------------------------------------
|
|
379
408
|
#
|
|
380
409
|
# PythonJob search data serializer from the `aiida.data` entry point by the
|
|
381
410
|
# module name and class name (e.g., `ase.atoms.Atoms`).
|
|
@@ -24,6 +24,7 @@ dependencies = [
|
|
|
24
24
|
"aiida-core>=2.3,<3",
|
|
25
25
|
"ase",
|
|
26
26
|
"cloudpickle",
|
|
27
|
+
"node-graph==0.2.22",
|
|
27
28
|
]
|
|
28
29
|
|
|
29
30
|
[project.optional-dependencies]
|
|
@@ -163,3 +164,6 @@ features = ["docs"]
|
|
|
163
164
|
build = [
|
|
164
165
|
"make -C docs"
|
|
165
166
|
]
|
|
167
|
+
|
|
168
|
+
[tool.hatch.metadata]
|
|
169
|
+
allow-direct-references = true
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""AiiDA plugin that run Python function on remote computers."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.
|
|
3
|
+
__version__ = "0.3.0"
|
|
4
|
+
|
|
5
|
+
from node_graph import spec
|
|
4
6
|
|
|
5
7
|
from .calculations import PythonJob
|
|
6
8
|
from .decorator import pyfunction
|
|
@@ -13,4 +15,5 @@ __all__ = (
|
|
|
13
15
|
"PickledData",
|
|
14
16
|
"prepare_pythonjob_inputs",
|
|
15
17
|
"PythonJobParser",
|
|
18
|
+
"spec",
|
|
16
19
|
)
|
{aiida_pythonjob-0.2.5 → aiida_pythonjob-0.3.0}/src/aiida_pythonjob/calculations/pyfunction.py
RENAMED
|
@@ -5,6 +5,8 @@ from __future__ import annotations
|
|
|
5
5
|
import traceback
|
|
6
6
|
import typing as t
|
|
7
7
|
|
|
8
|
+
import cloudpickle
|
|
9
|
+
import plumpy
|
|
8
10
|
from aiida.common.lang import override
|
|
9
11
|
from aiida.engine import Process, ProcessSpec
|
|
10
12
|
from aiida.engine.processes.exit_code import ExitCode
|
|
@@ -30,10 +32,18 @@ class PyFunction(Process):
|
|
|
30
32
|
super().__init__(enable_persistence=False, *args, **kwargs) # type: ignore[misc]
|
|
31
33
|
self._func = None
|
|
32
34
|
|
|
35
|
+
@override
|
|
36
|
+
def load_instance_state(
|
|
37
|
+
self, saved_state: t.MutableMapping[str, t.Any], load_context: plumpy.persistence.LoadSaveContext
|
|
38
|
+
) -> None:
|
|
39
|
+
"""Load the instance state from the saved state."""
|
|
40
|
+
|
|
41
|
+
super().load_instance_state(saved_state, load_context)
|
|
42
|
+
# Restore the function from the pickled data
|
|
43
|
+
self._func = cloudpickle.loads(self.inputs.function_data.pickled_function)
|
|
44
|
+
|
|
33
45
|
@property
|
|
34
46
|
def func(self) -> t.Callable[..., t.Any]:
|
|
35
|
-
import cloudpickle
|
|
36
|
-
|
|
37
47
|
if self._func is None:
|
|
38
48
|
self._func = cloudpickle.loads(self.inputs.function_data.pickled_function)
|
|
39
49
|
return self._func
|
|
@@ -189,7 +199,8 @@ class PyFunction(Process):
|
|
|
189
199
|
if exit_code:
|
|
190
200
|
return exit_code
|
|
191
201
|
# Store the outputs
|
|
192
|
-
for
|
|
193
|
-
|
|
202
|
+
for name, port in self.output_ports["ports"].items():
|
|
203
|
+
if "value" in port:
|
|
204
|
+
self.out(name, port["value"])
|
|
194
205
|
|
|
195
206
|
return ExitCode()
|
|
@@ -60,8 +60,10 @@ def pyfunction(
|
|
|
60
60
|
manager = get_manager()
|
|
61
61
|
runner = manager.get_runner()
|
|
62
62
|
# # Remove all the known inputs from the kwargs
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
outputs_spec = kwargs.pop("outputs_spec", None) or outputs
|
|
64
|
+
inputs_spec = kwargs.pop("inputs_spec", None) or inputs
|
|
65
|
+
input_ports = kwargs.pop("input_ports", None)
|
|
66
|
+
output_ports = kwargs.pop("output_ports", None)
|
|
65
67
|
metadata = kwargs.pop("metadata", None)
|
|
66
68
|
function_data = kwargs.pop("function_data", None)
|
|
67
69
|
deserializers = kwargs.pop("deserializers", None)
|
|
@@ -73,6 +75,8 @@ def pyfunction(
|
|
|
73
75
|
process_inputs = prepare_pyfunction_inputs(
|
|
74
76
|
function=function,
|
|
75
77
|
function_inputs=function_inputs,
|
|
78
|
+
inputs_spec=inputs_spec,
|
|
79
|
+
outputs_spec=outputs_spec,
|
|
76
80
|
input_ports=input_ports,
|
|
77
81
|
output_ports=output_ports,
|
|
78
82
|
metadata=metadata,
|
|
@@ -2,23 +2,22 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import inspect
|
|
4
4
|
import os
|
|
5
|
-
from typing import Any, Callable, Dict,
|
|
5
|
+
from typing import Any, Callable, Dict, Optional, Union
|
|
6
6
|
|
|
7
7
|
from aiida import orm
|
|
8
|
+
from node_graph.nodes.utils import generate_input_sockets, generate_output_sockets
|
|
8
9
|
|
|
9
|
-
from .
|
|
10
|
-
|
|
11
|
-
build_input_port_definitions,
|
|
12
|
-
format_input_output_ports,
|
|
13
|
-
get_or_create_code,
|
|
14
|
-
)
|
|
10
|
+
from .ports_adapter import inputs_sockets_to_ports, outputs_sockets_to_ports
|
|
11
|
+
from .utils import build_function_data, get_or_create_code, serialize_ports
|
|
15
12
|
|
|
16
13
|
|
|
17
14
|
def prepare_pythonjob_inputs(
|
|
18
15
|
function: Optional[Callable[..., Any]] = None,
|
|
19
16
|
function_inputs: Optional[Dict[str, Any]] = None,
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
inputs_spec: Optional[type] = None,
|
|
18
|
+
outputs_spec: Optional[type] = None,
|
|
19
|
+
output_ports: Optional[Dict[str, Any]] = None,
|
|
20
|
+
input_ports: Optional[Dict[str, Any]] = None,
|
|
22
21
|
code: Optional[orm.AbstractCode] = None,
|
|
23
22
|
command_info: Optional[Dict[str, str]] = None,
|
|
24
23
|
computer: Union[str, orm.Computer] = "localhost",
|
|
@@ -32,7 +31,6 @@ def prepare_pythonjob_inputs(
|
|
|
32
31
|
**kwargs: Any,
|
|
33
32
|
) -> Dict[str, Any]:
|
|
34
33
|
"""Prepare the inputs for PythonJob"""
|
|
35
|
-
from .utils import serialize_ports
|
|
36
34
|
|
|
37
35
|
if function is None and function_data is None:
|
|
38
36
|
raise ValueError("Either function or function_data must be provided")
|
|
@@ -62,14 +60,18 @@ def prepare_pythonjob_inputs(
|
|
|
62
60
|
if code is None:
|
|
63
61
|
command_info = command_info or {}
|
|
64
62
|
code = get_or_create_code(computer=computer, **command_info)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
# outputs
|
|
64
|
+
if not output_ports:
|
|
65
|
+
node_outputs = generate_output_sockets(function or (lambda **_: None), outputs=outputs_spec)
|
|
66
|
+
output_ports = outputs_sockets_to_ports(node_outputs)
|
|
67
|
+
# inputs
|
|
68
|
+
if not input_ports:
|
|
69
|
+
node_inputs = generate_input_sockets(function or (lambda **_: None), inputs=inputs_spec)
|
|
70
|
+
input_ports = inputs_sockets_to_ports(node_inputs)
|
|
71
|
+
|
|
72
|
+
function_data["output_ports"] = output_ports
|
|
71
73
|
function_data["input_ports"] = input_ports
|
|
72
|
-
# serialize the
|
|
74
|
+
# serialize kwargs against the (nested) input schema
|
|
73
75
|
function_inputs = function_inputs or {}
|
|
74
76
|
function_inputs = serialize_ports(python_data=function_inputs, port_schema=input_ports, serializers=serializers)
|
|
75
77
|
# replace "." with "__dot__" in the keys of a dictionary
|
|
@@ -112,8 +114,10 @@ def create_inputs(func, *args: Any, **kwargs: Any) -> dict[str, Any]:
|
|
|
112
114
|
def prepare_pyfunction_inputs(
|
|
113
115
|
function: Optional[Callable[..., Any]] = None,
|
|
114
116
|
function_inputs: Optional[Dict[str, Any]] = None,
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
inputs_spec: Optional[type] = None,
|
|
118
|
+
outputs_spec: Optional[type] = None,
|
|
119
|
+
output_ports: Optional[Dict[str, Any]] = None,
|
|
120
|
+
input_ports: Optional[Dict[str, Any]] = None,
|
|
117
121
|
metadata: Optional[Dict[str, Any]] = None,
|
|
118
122
|
process_label: Optional[str] = None,
|
|
119
123
|
function_data: dict | None = None,
|
|
@@ -125,8 +129,6 @@ def prepare_pyfunction_inputs(
|
|
|
125
129
|
"""Prepare the inputs for PythonJob"""
|
|
126
130
|
import types
|
|
127
131
|
|
|
128
|
-
from .utils import serialize_ports
|
|
129
|
-
|
|
130
132
|
if function is None and function_data is None:
|
|
131
133
|
raise ValueError("Either function or function_data must be provided")
|
|
132
134
|
if function is not None and function_data is not None:
|
|
@@ -139,11 +141,16 @@ def prepare_pyfunction_inputs(
|
|
|
139
141
|
raise NotImplementedError("Built-in functions are not supported yet")
|
|
140
142
|
else:
|
|
141
143
|
raise ValueError("Invalid function type")
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
# outputs
|
|
145
|
+
if not output_ports:
|
|
146
|
+
node_outputs = generate_output_sockets(function or (lambda **_: None), outputs=outputs_spec)
|
|
147
|
+
output_ports = outputs_sockets_to_ports(node_outputs)
|
|
148
|
+
# inputs
|
|
149
|
+
if not input_ports:
|
|
150
|
+
node_inputs = generate_input_sockets(function or (lambda **_: None), inputs=inputs_spec)
|
|
151
|
+
input_ports = inputs_sockets_to_ports(node_inputs)
|
|
152
|
+
|
|
153
|
+
function_data["output_ports"] = output_ports
|
|
147
154
|
function_data["input_ports"] = input_ports
|
|
148
155
|
# serialize the kwargs into AiiDA Data
|
|
149
156
|
function_inputs = function_inputs or {}
|
|
@@ -72,11 +72,14 @@ class PythonJobParser(Parser):
|
|
|
72
72
|
return exit_code
|
|
73
73
|
|
|
74
74
|
# Store the outputs
|
|
75
|
-
for
|
|
76
|
-
|
|
75
|
+
for name, port in self.output_ports["ports"].items():
|
|
76
|
+
if "value" in port:
|
|
77
|
+
self.out(name, port["value"])
|
|
77
78
|
|
|
78
79
|
except OSError:
|
|
79
80
|
return self.exit_codes.ERROR_READING_OUTPUT_FILE
|
|
80
81
|
except ValueError as exception:
|
|
81
|
-
self.logger.error(
|
|
82
|
+
self.logger.error(
|
|
83
|
+
f"An error occurred when attempting to parse the output of the calculation: ValueError: {exception!s}"
|
|
84
|
+
)
|
|
82
85
|
return self.exit_codes.ERROR_INVALID_OUTPUT
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Dict, Optional
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _is_namespace_identifier(identifier: str, *, ns_id: str = "node_graph.namespace") -> bool:
|
|
7
|
+
return identifier == ns_id or identifier.endswith(".namespace")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _socket_meta_required(sock: Dict[str, Any]) -> Optional[bool]:
|
|
11
|
+
meta = sock.get("metadata", {}) or {}
|
|
12
|
+
if "required" in meta and meta["required"] is not None:
|
|
13
|
+
return bool(meta["required"])
|
|
14
|
+
if sock.get("property", {}).get("default", None) is not None:
|
|
15
|
+
return False
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _socket_meta_help(sock: Dict[str, Any]) -> Optional[str]:
|
|
20
|
+
return (sock.get("metadata") or {}).get("help")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _socket_to_port_object(sock: Dict[str, Any]) -> Dict[str, Any]:
|
|
24
|
+
ident = sock.get("identifier", "")
|
|
25
|
+
meta = sock.get("metadata", {}) or {}
|
|
26
|
+
|
|
27
|
+
if _is_namespace_identifier(ident):
|
|
28
|
+
ports: Dict[str, Dict[str, Any]] = {}
|
|
29
|
+
for name, child in (sock.get("sockets") or {}).items():
|
|
30
|
+
ports[name] = _socket_to_port_object(child)
|
|
31
|
+
|
|
32
|
+
obj: Dict[str, Any] = {"identifier": "NAMESPACE", "ports": ports}
|
|
33
|
+
|
|
34
|
+
if meta.get("dynamic"):
|
|
35
|
+
obj["dynamic"] = True
|
|
36
|
+
# Prefer full nested item namespace when present
|
|
37
|
+
if isinstance(meta.get("item"), dict):
|
|
38
|
+
obj["item"] = _socket_to_port_object(meta["item"])
|
|
39
|
+
else:
|
|
40
|
+
# fallback: identifier only (treat namespace id as empty namespace)
|
|
41
|
+
item_ident = meta.get("item_identifier", "node_graph.any")
|
|
42
|
+
obj["item"] = (
|
|
43
|
+
{"identifier": "NAMESPACE", "ports": {}}
|
|
44
|
+
if _is_namespace_identifier(item_ident)
|
|
45
|
+
else {"identifier": "ANY"}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
h = _socket_meta_help(sock)
|
|
49
|
+
if h:
|
|
50
|
+
obj["help"] = h
|
|
51
|
+
req = _socket_meta_required(sock)
|
|
52
|
+
if req is not None:
|
|
53
|
+
obj["required"] = req
|
|
54
|
+
return obj
|
|
55
|
+
|
|
56
|
+
# Leaf
|
|
57
|
+
obj = {"identifier": "ANY"}
|
|
58
|
+
h = _socket_meta_help(sock)
|
|
59
|
+
if h:
|
|
60
|
+
obj["help"] = h
|
|
61
|
+
req = _socket_meta_required(sock)
|
|
62
|
+
if req is not None:
|
|
63
|
+
obj["required"] = req
|
|
64
|
+
return obj
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def inputs_sockets_to_ports(node_inputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
68
|
+
ports_map: Dict[str, Dict[str, Any]] = {}
|
|
69
|
+
for name, sock in (node_inputs.get("sockets") or {}).items():
|
|
70
|
+
ports_map[name] = _socket_to_port_object(sock)
|
|
71
|
+
|
|
72
|
+
schema: Dict[str, Any] = {"name": "inputs", "identifier": "NAMESPACE", "ports": ports_map}
|
|
73
|
+
meta = node_inputs.get("metadata", {}) or {}
|
|
74
|
+
if meta.get("dynamic"):
|
|
75
|
+
schema["dynamic"] = True
|
|
76
|
+
if isinstance(meta.get("item"), dict):
|
|
77
|
+
schema["item"] = _socket_to_port_object(meta["item"])
|
|
78
|
+
else:
|
|
79
|
+
item_ident = meta.get("item_identifier", "node_graph.any")
|
|
80
|
+
schema["item"] = (
|
|
81
|
+
{"identifier": "NAMESPACE", "ports": {}}
|
|
82
|
+
if _is_namespace_identifier(item_ident)
|
|
83
|
+
else {"identifier": "ANY"}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return schema
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def outputs_sockets_to_ports(node_outputs: Dict[str, Any]) -> Dict[str, Any]:
|
|
90
|
+
meta = node_outputs.get("metadata", {}) or {}
|
|
91
|
+
sockets = node_outputs.get("sockets") or {}
|
|
92
|
+
|
|
93
|
+
ports_map = {name: _socket_to_port_object(sock) for name, sock in sockets.items()}
|
|
94
|
+
schema: Dict[str, Any] = {"name": "outputs", "identifier": "NAMESPACE", "ports": ports_map}
|
|
95
|
+
if meta.get("dynamic"):
|
|
96
|
+
schema["dynamic"] = True
|
|
97
|
+
if isinstance(meta.get("item"), dict):
|
|
98
|
+
schema["item"] = _socket_to_port_object(meta["item"])
|
|
99
|
+
else:
|
|
100
|
+
item_ident = meta.get("item_identifier", "node_graph.any")
|
|
101
|
+
schema["item"] = (
|
|
102
|
+
{"identifier": "NAMESPACE", "ports": {}}
|
|
103
|
+
if _is_namespace_identifier(item_ident)
|
|
104
|
+
else {"identifier": "ANY"}
|
|
105
|
+
)
|
|
106
|
+
return schema
|