aiida-pythonjob 0.2.1__tar.gz → 0.2.2__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.1 → aiida_pythonjob-0.2.2}/PKG-INFO +1 -1
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/__init__.py +1 -1
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/calculations/pyfunction.py +32 -12
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/calculations/pythonjob.py +15 -20
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/utils.py +2 -4
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/test_pyfunction.py +13 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/test_pythonjob.py +19 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/test_utils.py +2 -9
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/.github/workflows/ci.yml +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/.github/workflows/python-publish.yml +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/.gitignore +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/.pre-commit-config.yaml +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/.readthedocs.yml +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/LICENSE +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/README.md +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/Makefile +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/environment.yml +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/gallery/autogen/GALLERY_HEADER.rst +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/gallery/autogen/pyfunction.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/gallery/autogen/pythonjob.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/make.bat +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/requirements.txt +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/source/conf.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/source/index.rst +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/source/installation.rst +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/source/tutorial/dft.ipynb +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/source/tutorial/html/atomization_energy.html +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/source/tutorial/html/pythonjob_eos_emt.html +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/source/tutorial/index.rst +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/examples/test_add.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/pyproject.toml +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/calculations/__init__.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/calculations/utils.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/config.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/data/__init__.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/data/atoms.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/data/deserializer.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/data/pickled_data.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/data/serializer.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/decorator.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/launch.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/parsers/__init__.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/parsers/pythonjob.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/conftest.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/inputs_folder/another_input.txt +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/test_create_env.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/test_data.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/test_entry_points.py +0 -0
- {aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/tests/test_parser.py +0 -0
{aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/calculations/pyfunction.py
RENAMED
|
@@ -38,20 +38,18 @@ class PyFunction(Process):
|
|
|
38
38
|
|
|
39
39
|
@property
|
|
40
40
|
def func(self) -> t.Callable[..., t.Any]:
|
|
41
|
+
import cloudpickle
|
|
42
|
+
|
|
41
43
|
if self._func is None:
|
|
42
|
-
self._func = self.inputs.function_data.pickled_function
|
|
44
|
+
self._func = cloudpickle.loads(self.inputs.function_data.pickled_function)
|
|
43
45
|
return self._func
|
|
44
46
|
|
|
45
47
|
@classmethod
|
|
46
48
|
def define(cls, spec: ProcessSpec) -> None: # type: ignore[override]
|
|
47
49
|
"""Define the process specification, including its inputs, outputs and known exit codes."""
|
|
48
50
|
super().define(spec)
|
|
49
|
-
spec.input_namespace("function_data")
|
|
50
|
-
spec.input("function_data.name", valid_type=Str, serializer=to_aiida_type)
|
|
51
|
-
spec.input("function_data.source_code", valid_type=Str, serializer=to_aiida_type, required=False)
|
|
51
|
+
spec.input_namespace("function_data", dynamic=True, required=True)
|
|
52
52
|
spec.input("function_data.outputs", valid_type=List, serializer=to_aiida_type, required=False)
|
|
53
|
-
spec.input("function_data.pickled_function", valid_type=Data, required=False)
|
|
54
|
-
spec.input("function_data.mode", valid_type=Str, serializer=to_aiida_type, required=False)
|
|
55
53
|
spec.input("process_label", valid_type=Str, serializer=to_aiida_type, required=False)
|
|
56
54
|
spec.input_namespace("function_inputs", valid_type=Data, required=False)
|
|
57
55
|
spec.input(
|
|
@@ -88,10 +86,10 @@ class PyFunction(Process):
|
|
|
88
86
|
def get_function_name(self) -> str:
|
|
89
87
|
"""Return the name of the function to run."""
|
|
90
88
|
if "name" in self.inputs.function_data:
|
|
91
|
-
name = self.inputs.function_data.name
|
|
89
|
+
name = self.inputs.function_data.name
|
|
92
90
|
else:
|
|
93
91
|
try:
|
|
94
|
-
name = self.
|
|
92
|
+
name = self.func.__name__
|
|
95
93
|
except AttributeError:
|
|
96
94
|
# If a user doesn't specify name, fallback to something generic
|
|
97
95
|
name = "anonymous_function"
|
|
@@ -202,8 +200,10 @@ class PyFunction(Process):
|
|
|
202
200
|
if exit_code.status != 0:
|
|
203
201
|
return exit_code
|
|
204
202
|
if len(top_level_output_list) == 1:
|
|
205
|
-
#
|
|
206
|
-
if
|
|
203
|
+
# User returned a single (nested) dict with AiiDA data nodes as values
|
|
204
|
+
if self.already_serialized(results):
|
|
205
|
+
top_level_output_list = [{"name": key, "value": value} for key, value in results.items()]
|
|
206
|
+
elif top_level_output_list[0]["name"] in results:
|
|
207
207
|
top_level_output_list[0]["value"] = self.serialize_output(
|
|
208
208
|
results.pop(top_level_output_list[0]["name"]),
|
|
209
209
|
top_level_output_list[0],
|
|
@@ -228,8 +228,14 @@ class PyFunction(Process):
|
|
|
228
228
|
if len(results) > 0:
|
|
229
229
|
self.logger.warning(f"Found extra results that are not included in the output: {results.keys()}")
|
|
230
230
|
elif len(top_level_output_list) == 1:
|
|
231
|
-
# Single top-level output
|
|
232
|
-
|
|
231
|
+
# Single top-level output
|
|
232
|
+
# There are two cases:
|
|
233
|
+
# 1. The output as a whole will be serialized as the single output
|
|
234
|
+
# 2. The output is a mapping with already AiiDA data nodes as values, no need to serialize
|
|
235
|
+
if self.already_serialized(results):
|
|
236
|
+
top_level_output_list[0]["value"] = results
|
|
237
|
+
else:
|
|
238
|
+
top_level_output_list[0]["value"] = self.serialize_output(results, top_level_output_list[0])
|
|
233
239
|
else:
|
|
234
240
|
return self.exit_codes.ERROR_RESULT_OUTPUT_MISMATCH
|
|
235
241
|
# Store the outputs
|
|
@@ -238,6 +244,20 @@ class PyFunction(Process):
|
|
|
238
244
|
|
|
239
245
|
return ExitCode()
|
|
240
246
|
|
|
247
|
+
def already_serialized(self, results):
|
|
248
|
+
"""Check if the results are already serialized."""
|
|
249
|
+
import collections
|
|
250
|
+
|
|
251
|
+
if isinstance(results, Data):
|
|
252
|
+
return True
|
|
253
|
+
elif isinstance(results, collections.abc.Mapping):
|
|
254
|
+
for value in results.values():
|
|
255
|
+
if not self.already_serialized(value):
|
|
256
|
+
return False
|
|
257
|
+
return True
|
|
258
|
+
else:
|
|
259
|
+
return False
|
|
260
|
+
|
|
241
261
|
def find_output(self, name):
|
|
242
262
|
"""Find the output spec with the given name."""
|
|
243
263
|
for output in self.output_list:
|
{aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/calculations/pythonjob.py
RENAMED
|
@@ -7,6 +7,7 @@ import typing as t
|
|
|
7
7
|
|
|
8
8
|
from aiida.common.datastructures import CalcInfo, CodeInfo
|
|
9
9
|
from aiida.common.folders import Folder
|
|
10
|
+
from aiida.common.lang import override
|
|
10
11
|
from aiida.engine import CalcJob, CalcJobProcessSpec
|
|
11
12
|
from aiida.orm import (
|
|
12
13
|
Data,
|
|
@@ -37,17 +38,14 @@ class PythonJob(CalcJob):
|
|
|
37
38
|
_DEFAULT_INPUT_FILE = "script.py"
|
|
38
39
|
_DEFAULT_OUTPUT_FILE = "aiida.out"
|
|
39
40
|
_DEFAULT_PARENT_FOLDER_NAME = "./parent_folder/"
|
|
41
|
+
_SOURCE_CODE_KEY = "source_code"
|
|
40
42
|
|
|
41
43
|
@classmethod
|
|
42
44
|
def define(cls, spec: CalcJobProcessSpec) -> None: # type: ignore[override]
|
|
43
45
|
"""Define the process specification, including its inputs, outputs and known exit codes."""
|
|
44
46
|
super().define(spec)
|
|
45
|
-
spec.input_namespace("function_data")
|
|
46
|
-
spec.input("function_data.name", valid_type=Str, serializer=to_aiida_type)
|
|
47
|
-
spec.input("function_data.source_code", valid_type=Str, serializer=to_aiida_type, required=False)
|
|
47
|
+
spec.input_namespace("function_data", dynamic=True, required=True)
|
|
48
48
|
spec.input("function_data.outputs", valid_type=List, serializer=to_aiida_type, required=False)
|
|
49
|
-
spec.input("function_data.pickled_function", valid_type=Data, required=False)
|
|
50
|
-
spec.input("function_data.mode", valid_type=Str, serializer=to_aiida_type, required=False)
|
|
51
49
|
spec.input("process_label", valid_type=Str, serializer=to_aiida_type, required=False)
|
|
52
50
|
spec.input_namespace("function_inputs", valid_type=Data, required=False)
|
|
53
51
|
spec.input(
|
|
@@ -175,13 +173,9 @@ class PythonJob(CalcJob):
|
|
|
175
173
|
def get_function_name(self) -> str:
|
|
176
174
|
"""Return the name of the function to run."""
|
|
177
175
|
if "name" in self.inputs.function_data:
|
|
178
|
-
name = self.inputs.function_data.name
|
|
176
|
+
name = self.inputs.function_data.name
|
|
179
177
|
else:
|
|
180
|
-
|
|
181
|
-
name = self.inputs.function_data.pickled_function.value.__name__
|
|
182
|
-
except AttributeError:
|
|
183
|
-
# If a user doesn't specify name, fallback to something generic
|
|
184
|
-
name = "anonymous_function"
|
|
178
|
+
name = "anonymous_function"
|
|
185
179
|
return name
|
|
186
180
|
|
|
187
181
|
def _build_process_label(self) -> str:
|
|
@@ -192,6 +186,13 @@ class PythonJob(CalcJob):
|
|
|
192
186
|
name = self.get_function_name()
|
|
193
187
|
return f"PythonJob<{name}>"
|
|
194
188
|
|
|
189
|
+
@override
|
|
190
|
+
def _setup_db_record(self) -> None:
|
|
191
|
+
"""Set up the database record for the process."""
|
|
192
|
+
super()._setup_db_record()
|
|
193
|
+
if "source_code" in self.inputs.function_data:
|
|
194
|
+
self.node.base.attributes.set(self._SOURCE_CODE_KEY, self.inputs.function_data.source_code)
|
|
195
|
+
|
|
195
196
|
def on_create(self) -> None:
|
|
196
197
|
"""Called when a Process is created."""
|
|
197
198
|
super().on_create()
|
|
@@ -223,19 +224,13 @@ class PythonJob(CalcJob):
|
|
|
223
224
|
else:
|
|
224
225
|
parent_folder_name = self._DEFAULT_PARENT_FOLDER_NAME
|
|
225
226
|
|
|
226
|
-
function_data = self.inputs.function_data
|
|
227
|
-
|
|
228
227
|
# Build the Python script
|
|
229
|
-
source_code =
|
|
230
|
-
|
|
231
|
-
pickled_function = self.inputs.function_data.pickled_function.get_serialized_value()
|
|
232
|
-
else:
|
|
233
|
-
pickled_function = None
|
|
234
|
-
# Generate script.py content
|
|
228
|
+
source_code = self.node.base.attributes.get(self._SOURCE_CODE_KEY, None)
|
|
229
|
+
pickled_function = self.inputs.function_data.pickled_function
|
|
235
230
|
function_name = self.get_function_name() # or some user-defined name
|
|
236
231
|
script_content = generate_script_py(
|
|
237
232
|
pickled_function=pickled_function,
|
|
238
|
-
source_code=source_code
|
|
233
|
+
source_code=source_code,
|
|
239
234
|
function_name=function_name,
|
|
240
235
|
)
|
|
241
236
|
|
|
@@ -54,8 +54,6 @@ def inspect_function(
|
|
|
54
54
|
# the source code is not saved in the pickle file
|
|
55
55
|
import cloudpickle
|
|
56
56
|
|
|
57
|
-
from aiida_pythonjob.data.pickled_data import PickledData
|
|
58
|
-
|
|
59
57
|
if inspect_source:
|
|
60
58
|
try:
|
|
61
59
|
source_code = inspect.getsource(func)
|
|
@@ -70,10 +68,10 @@ def inspect_function(
|
|
|
70
68
|
if register_pickle_by_value:
|
|
71
69
|
module = importlib.import_module(func.__module__)
|
|
72
70
|
cloudpickle.register_pickle_by_value(module)
|
|
73
|
-
pickled_function =
|
|
71
|
+
pickled_function = cloudpickle.dumps(func)
|
|
74
72
|
cloudpickle.unregister_pickle_by_value(module)
|
|
75
73
|
else:
|
|
76
|
-
pickled_function =
|
|
74
|
+
pickled_function = cloudpickle.dumps(func)
|
|
77
75
|
|
|
78
76
|
return {"source_code": source_code, "mode": "use_pickled_function", "pickled_function": pickled_function}
|
|
79
77
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from aiida import orm
|
|
1
2
|
from aiida.engine import run_get_node
|
|
2
3
|
from aiida_pythonjob import pyfunction
|
|
3
4
|
|
|
@@ -118,3 +119,15 @@ def test_override_outputs():
|
|
|
118
119
|
assert result["add_multiply"]["add"]["order1"].value == 3
|
|
119
120
|
assert result["add_multiply"]["add"]["order2"].value == 5
|
|
120
121
|
assert result["add_multiply"]["multiply"].value == 2
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_aiida_node_as_inputs_outputs():
|
|
125
|
+
"""Test function with AiiDA nodes as inputs and outputs."""
|
|
126
|
+
|
|
127
|
+
@pyfunction()
|
|
128
|
+
def add(x, y):
|
|
129
|
+
return {"sum": orm.Int(x + y), "diff": orm.Int(x - y)}
|
|
130
|
+
|
|
131
|
+
result, node = run_get_node(add, x=orm.Int(1), y=orm.Int(2))
|
|
132
|
+
assert set(result.keys()) == {"sum", "diff"}
|
|
133
|
+
assert result["sum"].value == 3
|
|
@@ -310,3 +310,22 @@ def test_local_function(fixture_localhost):
|
|
|
310
310
|
)
|
|
311
311
|
result, node = run_get_node(PythonJob, **inputs)
|
|
312
312
|
assert result["result"].value == 8
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
@pytest.mark.usefixtures("started_daemon_client")
|
|
316
|
+
def test_submit(fixture_localhost):
|
|
317
|
+
"""Test decorator."""
|
|
318
|
+
from aiida.engine import submit
|
|
319
|
+
|
|
320
|
+
def add(x, y):
|
|
321
|
+
return x + y
|
|
322
|
+
|
|
323
|
+
inputs = prepare_pythonjob_inputs(
|
|
324
|
+
add,
|
|
325
|
+
function_inputs={"x": 1, "y": 2},
|
|
326
|
+
process_label="add",
|
|
327
|
+
)
|
|
328
|
+
node = submit(PythonJob, **inputs, wait=True)
|
|
329
|
+
|
|
330
|
+
assert node.outputs.result.value == 3
|
|
331
|
+
assert node.process_label == "add"
|
|
@@ -12,19 +12,12 @@ def test_build_function_data():
|
|
|
12
12
|
assert function_data["name"] == "build_function_data"
|
|
13
13
|
assert "source_code" in function_data
|
|
14
14
|
assert "pickled_function" in function_data
|
|
15
|
-
|
|
16
|
-
with node.base.repository.open(node.FILENAME, mode="rb") as f:
|
|
17
|
-
text = f.read()
|
|
18
|
-
assert b"cloudpickle" not in text
|
|
19
|
-
|
|
15
|
+
assert b"cloudpickle" not in function_data["pickled_function"]
|
|
20
16
|
function_data = build_function_data(build_function_data, register_pickle_by_value=True)
|
|
21
17
|
assert function_data["name"] == "build_function_data"
|
|
22
18
|
assert "source_code" in function_data
|
|
23
19
|
assert "pickled_function" in function_data
|
|
24
|
-
|
|
25
|
-
with node.base.repository.open(node.FILENAME, mode="rb") as f:
|
|
26
|
-
text = f.read()
|
|
27
|
-
assert b"cloudpickle" in text
|
|
20
|
+
assert b"cloudpickle" in function_data["pickled_function"]
|
|
28
21
|
|
|
29
22
|
def local_function(x, y):
|
|
30
23
|
return x + y
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/source/tutorial/html/atomization_energy.html
RENAMED
|
File without changes
|
{aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/docs/source/tutorial/html/pythonjob_eos_emt.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{aiida_pythonjob-0.2.1 → aiida_pythonjob-0.2.2}/src/aiida_pythonjob/calculations/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|