Skip to content

Commit de32baa

Browse files
authored
Merge pull request #341 from simvue-io/hotfix/add-missing-tests
Refactor serialization and fix tuple without comma bug
2 parents 4517e63 + e60f19c commit de32baa

35 files changed

+340
-211
lines changed

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,13 +69,13 @@ if __name__ == "__main__":
6969
description='This is part 1 of a test') # Description
7070

7171
# Upload the code
72-
run.save('training.py', 'code')
72+
run.save_file('training.py', 'code')
7373

7474
# Upload an input file
75-
run.save('params.in', 'input')
75+
run.save_file('params.in', 'input')
7676

7777
# Add an alert (the alert definition will be created if necessary)
78-
run.add_alert(name='loss-too-high', # Name
78+
run.create_alert(name='loss-too-high', # Name
7979
source='metrics', # Source
8080
rule='is above', # Rule
8181
metric='loss', # Metric
@@ -96,7 +96,7 @@ if __name__ == "__main__":
9696
...
9797

9898
# Upload an output file
99-
run.save('output.cdf', 'output')
99+
run.save_file('output.cdf', 'output')
100100

101101
# If we weren't using a context manager we'd need to end the run
102102
# run.close()

examples/GeometryOptimisation/bluemira_simvue_geometry_optimisation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,5 +171,5 @@ def my_minimise_length(vector, grad, parameterisation, ad_args=None):
171171

172172
# Here we're minimising the length, within the bounds of our PrincetonD parameterisation,
173173
# so we'd expect that x1 goes to its upper bound, and x2 goes to its lower bound.
174-
run.save("bluemira_simvue_geometry_optimisation.py", "code")
174+
run.save_file("bluemira_simvue_geometry_optimisation.py", "code")
175175
run.close()

examples/PyTorch/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -205,7 +205,7 @@ def main():
205205
scheduler.step()
206206

207207
if args.save_model:
208-
run.save(model.state_dict(), "output", name="mnist_cnn.pt")
208+
run.save_file(model.state_dict(), "output", name="mnist_cnn.pt")
209209

210210
run.close()
211211

examples/SU2/SU2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
filetype = None
5757
if input_file.endswith(".cfg"):
5858
filetype = "text/plain"
59-
run.save(input_file, "input", filetype)
59+
run.save_file(input_file, "input", filetype)
6060

6161
running = True
6262
latest = []
@@ -106,6 +106,6 @@
106106

107107
# Save output files
108108
for output_file in OUTPUT_FILES:
109-
run.save(output_file, "output")
109+
run.save_file(output_file, "output")
110110

111111
run.close()

examples/Tensorflow/dynamic_rnn.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@
4545
"computation over sequences with variable length. This example is using a toy dataset to "
4646
"classify linear sequences. The generated sequences have variable length.",
4747
)
48-
run.save("dynamic_rnn.py", "code")
48+
run.save_file("dynamic_rnn.py", "code")
4949

5050
# ====================
5151
# TOY DATA GENERATOR

simvue/client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
to_dataframe,
2020
parse_run_set_metrics,
2121
)
22-
from .serialization import Deserializer
22+
from .serialization import deserialize_data
2323
from .types import DeserializedContent
2424
from .utilities import check_extra, get_auth
2525

@@ -608,7 +608,7 @@ def get_artifact(
608608
response = requests.get(url, timeout=DOWNLOAD_TIMEOUT)
609609
response.raise_for_status()
610610

611-
content: typing.Optional[DeserializedContent] = Deserializer().deserialize(
611+
content: typing.Optional[DeserializedContent] = deserialize_data(
612612
response.content, mimetype, allow_pickle
613613
)
614614

simvue/executor.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -167,10 +167,10 @@ def callback_function(status_code: int, std_out: str, std_err: str) -> None:
167167
)
168168

169169
if script:
170-
self._runner.save(filename=script, category="code")
170+
self._runner.save_file(file_path=script, category="code")
171171

172172
if input_file:
173-
self._runner.save(filename=input_file, category="input")
173+
self._runner.save_file(file_path=input_file, category="input")
174174

175175
_command: typing.List[str] = []
176176

@@ -284,11 +284,11 @@ def _save_output(self) -> None:
284284
for proc_id in self._exit_codes.keys():
285285
# Only save the file if the contents are not empty
286286
if self._std_err[proc_id]:
287-
self._runner.save(
287+
self._runner.save_file(
288288
f"{self._runner.name}_{proc_id}.err", category="output"
289289
)
290290
if self._std_out[proc_id]:
291-
self._runner.save(
291+
self._runner.save_file(
292292
f"{self._runner.name}_{proc_id}.out", category="output"
293293
)
294294

simvue/run.py

Lines changed: 112 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from .factory.proxy import Simvue
3838
from .metrics import get_gpu_metrics, get_process_cpu, get_process_memory
3939
from .models import RunInput
40-
from .serialization import Serializer
40+
from .serialization import serialize_object
4141
from .system import get_system
4242
from .metadata import git_info
4343
from .utilities import (
@@ -159,7 +159,7 @@ def __exit__(
159159
else:
160160
if self._active:
161161
self.log_event(f"{exc_type.__name__}: {value}")
162-
if exc_type.__name__ in ("KeyboardInterrupt") and self._active:
162+
if exc_type.__name__ in ("KeyboardInterrupt",) and self._active:
163163
self.set_status("terminated")
164164
else:
165165
if traceback and self._active:
@@ -982,17 +982,87 @@ def log_metrics(
982982
@check_run_initialised
983983
@skip_if_failed("_aborted", "_suppress_errors", False)
984984
@pydantic.validate_call
985-
def save(
985+
def save_object(
986986
self,
987-
filename: str,
987+
obj: typing.Any,
988988
category: typing.Literal["input", "output", "code"],
989-
filetype: typing.Optional[str] = None,
990-
preserve_path: bool = False,
991989
name: typing.Optional[str] = None,
992990
allow_pickle: bool = False,
993991
) -> bool:
992+
"""Save an object to the Simvue server
993+
994+
Parameters
995+
----------
996+
obj : typing.Any
997+
object to serialize and send to the server
998+
category : Literal['input', 'output', 'code']
999+
category of file with respect to this run
1000+
name : str, optional
1001+
name to associate with this object, by default None
1002+
allow_pickle : bool, optional
1003+
whether to allow pickling if all other serialization types fail, by default False
1004+
1005+
Returns
1006+
-------
1007+
bool
1008+
whether object upload was successful
9941009
"""
995-
Upload file or object
1010+
serialized = serialize_object(obj, allow_pickle)
1011+
1012+
if not serialized or not (pickled := serialized[0]):
1013+
self._error(f"Failed to serialize '{obj}'")
1014+
return False
1015+
1016+
data_type = serialized[1]
1017+
1018+
if not data_type and not allow_pickle:
1019+
self._error("Unable to save Python object, set allow_pickle to True")
1020+
return False
1021+
1022+
data: dict[str, typing.Any] = {
1023+
"pickled": pickled,
1024+
"type": data_type,
1025+
"checksum": calculate_sha256(pickled, False),
1026+
"originalPath": "",
1027+
"size": sys.getsizeof(pickled),
1028+
"name": name,
1029+
"run": self._name,
1030+
"category": category,
1031+
"storage": self._storage_id,
1032+
}
1033+
1034+
# Register file
1035+
return self._simvue is not None and self._simvue.save_file(data) is not None
1036+
1037+
@skip_if_failed("_aborted", "_suppress_errors", False)
1038+
@pydantic.validate_call
1039+
def save_file(
1040+
self,
1041+
file_path: pydantic.FilePath,
1042+
category: typing.Literal["input", "output", "code"],
1043+
filetype: typing.Optional[str] = None,
1044+
preserve_path: bool = False,
1045+
name: typing.Optional[str] = None,
1046+
) -> bool:
1047+
"""Upload file to the server
1048+
1049+
Parameters
1050+
----------
1051+
file_path : pydantic.FilePath
1052+
path to the file to upload
1053+
category : Literal['input', 'output', 'code']
1054+
category of file with respect to this run
1055+
filetype : str, optional
1056+
the MIME file type else this is deduced, by default None
1057+
preserve_path : bool, optional
1058+
whether to preserve the path during storage, by default False
1059+
name : str, optional
1060+
name to associate with this file, by default None
1061+
1062+
Returns
1063+
-------
1064+
bool
1065+
whether the upload was successful
9961066
"""
9971067
if self._mode == "disabled":
9981068
return True
@@ -1005,96 +1075,48 @@ def save(
10051075
self._error("Cannot upload output files for runs in the created state")
10061076
return False
10071077

1008-
is_file: bool = False
1078+
mimetypes.init()
1079+
mimetypes_valid = ["application/vnd.plotly.v1+json"]
1080+
mimetypes_valid += list(mimetypes.types_map.values())
10091081

1010-
if isinstance(filename, str):
1011-
if not os.path.isfile(filename):
1012-
self._error(f"File {filename} does not exist")
1013-
return False
1014-
else:
1015-
is_file = True
1016-
1017-
if filetype:
1018-
mimetypes_valid = ["application/vnd.plotly.v1+json"]
1019-
mimetypes.init()
1020-
for _, value in mimetypes.types_map.items():
1021-
mimetypes_valid.append(value)
1022-
1023-
if filetype not in mimetypes_valid:
1024-
self._error("Invalid MIME type specified")
1025-
return False
1026-
1027-
data: dict[str, typing.Any] = {}
1028-
1029-
if preserve_path:
1030-
data["name"] = filename
1031-
if data["name"].startswith("./"):
1032-
data["name"] = data["name"][2:]
1033-
elif is_file:
1034-
data["name"] = os.path.basename(filename)
1035-
1036-
if name:
1037-
data["name"] = name
1038-
1039-
data["run"] = self._name
1040-
data["category"] = category
1082+
if filetype and filetype not in mimetypes_valid:
1083+
self._error(f"Invalid MIME type '{filetype}' specified")
1084+
return False
10411085

1042-
if is_file:
1043-
data["size"] = os.path.getsize(filename)
1044-
data["originalPath"] = os.path.abspath(
1045-
os.path.expanduser(os.path.expandvars(filename))
1046-
)
1047-
data["checksum"] = calculate_sha256(filename, is_file)
1086+
stored_file_name: str = f"{file_path}"
10481087

1049-
if data["size"] == 0:
1050-
click.secho(
1051-
"WARNING: saving zero-sized files not currently supported",
1052-
bold=True,
1053-
fg="yellow",
1054-
)
1055-
return True
1088+
if preserve_path and stored_file_name.startswith("./"):
1089+
stored_file_name = stored_file_name[2:]
1090+
elif not preserve_path:
1091+
stored_file_name = os.path.basename(file_path)
10561092

10571093
# Determine mimetype
1058-
mimetype = None
1059-
if not filetype and is_file:
1060-
mimetypes.init()
1061-
mimetype = mimetypes.guess_type(filename)[0]
1062-
if not mimetype:
1063-
mimetype = "application/octet-stream"
1064-
elif is_file:
1065-
mimetype = filetype
1066-
1067-
if mimetype:
1068-
data["type"] = mimetype
1069-
1070-
if not is_file:
1071-
serialized = Serializer().serialize(filename, allow_pickle)
1072-
1073-
if not serialized or not (pickled := serialized[0]):
1074-
self._error(f"Failed to serialize '{filename}'")
1075-
return False
1076-
1077-
data_type = serialized[1]
1078-
1079-
data["pickled"] = pickled
1080-
data["type"] = data_type
1081-
1082-
if not data["type"] and not allow_pickle:
1083-
self._error("Unable to save Python object, set allow_pickle to True")
1084-
return False
1094+
if not (mimetype := filetype):
1095+
mimetype = mimetypes.guess_type(file_path)[0] or "application/octet-stream"
10851096

1086-
data["checksum"] = calculate_sha256(pickled, False)
1087-
data["originalPath"] = ""
1088-
data["size"] = sys.getsizeof(pickled)
1097+
data: dict[str, typing.Any] = {
1098+
"name": name or stored_file_name,
1099+
"run": self._name,
1100+
"type": mimetype,
1101+
"storage": self._storage_id,
1102+
"category": category,
1103+
"size": (file_size := os.path.getsize(file_path)),
1104+
"originalPath": os.path.abspath(
1105+
os.path.expanduser(os.path.expandvars(file_path))
1106+
),
1107+
"checksum": calculate_sha256(f"{file_path}", True),
1108+
}
10891109

1090-
if self._storage_id:
1091-
data["storage"] = self._storage_id
1110+
if not file_size:
1111+
click.secho(
1112+
"WARNING: saving zero-sized files not currently supported",
1113+
bold=True,
1114+
fg="yellow",
1115+
)
1116+
return True
10921117

10931118
# Register file
1094-
if not self._simvue.save_file(data):
1095-
return False
1096-
1097-
return True
1119+
return self._simvue.save_file(data) is not None
10981120

10991121
@check_run_initialised
11001122
@skip_if_failed("_aborted", "_suppress_errors", False)
@@ -1129,7 +1151,7 @@ def save_directory(
11291151
for dirpath, _, filenames in directory.walk():
11301152
for filename in filenames:
11311153
if (full_path := dirpath.joinpath(filename)).is_file():
1132-
self.save(f"{full_path}", category, filetype, preserve_path)
1154+
self.save_file(full_path, category, filetype, preserve_path)
11331155

11341156
return True
11351157

@@ -1153,7 +1175,7 @@ def save_all(
11531175

11541176
for item in items:
11551177
if item.is_file():
1156-
save_file = self.save(f"{item}", category, filetype, preserve_path)
1178+
save_file = self.save_file(item, category, filetype, preserve_path)
11571179
elif item.is_dir():
11581180
save_file = self.save_directory(item, category, filetype, preserve_path)
11591181
else:

0 commit comments

Comments
 (0)