nextmv 0.28.1.dev0__tar.gz → 0.28.3.dev0__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.
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/PKG-INFO +1 -1
- nextmv-0.28.3.dev0/nextmv/__about__.py +1 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/cloud/application.py +3 -2
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/cloud/client.py +9 -4
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/cloud/manifest.py +5 -3
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/cloud/package.py +16 -2
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/cloud/run.py +3 -3
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/input.py +2 -1
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/options.py +26 -9
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/output.py +10 -52
- nextmv-0.28.3.dev0/nextmv/serialization.py +67 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/test_options.py +63 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/test_output.py +1 -9
- nextmv-0.28.3.dev0/tests/test_serialization.py +53 -0
- nextmv-0.28.1.dev0/nextmv/__about__.py +0 -1
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/.gitignore +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/LICENSE +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/README.md +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/__entrypoint__.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/__init__.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/base_model.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/cloud/__init__.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/cloud/acceptance_test.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/cloud/account.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/cloud/batch_experiment.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/cloud/input_set.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/cloud/instance.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/cloud/safe.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/cloud/scenario.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/cloud/secrets.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/cloud/status.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/cloud/version.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/deprecated.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/logger.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/nextmv/model.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/pyproject.toml +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/requirements.txt +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/__init__.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/cloud/__init__.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/cloud/app.yaml +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/cloud/test_application.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/cloud/test_client.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/cloud/test_manifest.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/cloud/test_package.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/cloud/test_run.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/cloud/test_safe_name_id.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/cloud/test_scenario.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/scripts/__init__.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/scripts/options1.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/scripts/options2.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/scripts/options3.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/scripts/options4.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/scripts/options5.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/scripts/options6.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/scripts/options7.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/scripts/options_deprecated.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/test_base_model.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/test_entrypoint/__init__.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/test_entrypoint/test_entrypoint.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/test_input.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/test_logger.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/test_model.py +0 -0
- {nextmv-0.28.1.dev0 → nextmv-0.28.3.dev0}/tests/test_version.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "v0.28.3.dev0"
|
|
@@ -66,6 +66,7 @@ from nextmv.logger import log
|
|
|
66
66
|
from nextmv.model import Model, ModelConfiguration
|
|
67
67
|
from nextmv.options import Options
|
|
68
68
|
from nextmv.output import Output
|
|
69
|
+
from nextmv.serialization import serialize_json
|
|
69
70
|
|
|
70
71
|
# Maximum size of the run input/output in bytes. This constant defines the
|
|
71
72
|
# maximum allowed size for run inputs and outputs. When the size exceeds this
|
|
@@ -1588,7 +1589,7 @@ class Application:
|
|
|
1588
1589
|
if isinstance(v, str):
|
|
1589
1590
|
options_dict[k] = v
|
|
1590
1591
|
else:
|
|
1591
|
-
options_dict[k] =
|
|
1592
|
+
options_dict[k] = serialize_json(v)
|
|
1592
1593
|
|
|
1593
1594
|
payload = {}
|
|
1594
1595
|
if upload_id_used:
|
|
@@ -2830,7 +2831,7 @@ class Application:
|
|
|
2830
2831
|
"""
|
|
2831
2832
|
|
|
2832
2833
|
if isinstance(input, dict):
|
|
2833
|
-
input =
|
|
2834
|
+
input = serialize_json(input)
|
|
2834
2835
|
|
|
2835
2836
|
self.client.upload_to_presigned_url(
|
|
2836
2837
|
url=upload_url.upload_url,
|
|
@@ -14,7 +14,6 @@ get_size(obj)
|
|
|
14
14
|
Finds the size of an object in bytes.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
|
-
import json
|
|
18
17
|
import os
|
|
19
18
|
from dataclasses import dataclass, field
|
|
20
19
|
from typing import IO, Any, Optional, Union
|
|
@@ -24,6 +23,8 @@ import requests
|
|
|
24
23
|
import yaml
|
|
25
24
|
from requests.adapters import HTTPAdapter, Retry
|
|
26
25
|
|
|
26
|
+
from nextmv.serialization import serialize_json
|
|
27
|
+
|
|
27
28
|
_MAX_LAMBDA_PAYLOAD_SIZE: int = 500 * 1024 * 1024
|
|
28
29
|
"""int: Maximum size of the payload handled by the Nextmv Cloud API.
|
|
29
30
|
|
|
@@ -293,7 +294,11 @@ class Client:
|
|
|
293
294
|
if data is not None:
|
|
294
295
|
kwargs["data"] = data
|
|
295
296
|
if payload is not None:
|
|
296
|
-
|
|
297
|
+
if isinstance(payload, (dict, list)):
|
|
298
|
+
data = serialize_json(payload)
|
|
299
|
+
kwargs["data"] = data
|
|
300
|
+
else:
|
|
301
|
+
raise ValueError("payload must be a dictionary or a list")
|
|
297
302
|
if query_params is not None:
|
|
298
303
|
kwargs["params"] = query_params
|
|
299
304
|
|
|
@@ -341,7 +346,7 @@ class Client:
|
|
|
341
346
|
|
|
342
347
|
upload_data: Optional[str] = None
|
|
343
348
|
if isinstance(data, dict):
|
|
344
|
-
upload_data =
|
|
349
|
+
upload_data = serialize_json(data)
|
|
345
350
|
elif isinstance(data, str):
|
|
346
351
|
upload_data = data
|
|
347
352
|
else:
|
|
@@ -436,7 +441,7 @@ def get_size(obj: Union[dict[str, Any], IO[bytes], str]) -> int:
|
|
|
436
441
|
"""
|
|
437
442
|
|
|
438
443
|
if isinstance(obj, dict):
|
|
439
|
-
obj_str =
|
|
444
|
+
obj_str = serialize_json(obj)
|
|
440
445
|
return len(obj_str.encode("utf-8"))
|
|
441
446
|
|
|
442
447
|
elif hasattr(obj, "read"):
|
|
@@ -33,7 +33,7 @@ FILE_NAME
|
|
|
33
33
|
|
|
34
34
|
import os
|
|
35
35
|
from enum import Enum
|
|
36
|
-
from typing import Any, Optional
|
|
36
|
+
from typing import Any, Optional, Union
|
|
37
37
|
|
|
38
38
|
import yaml
|
|
39
39
|
from pydantic import AliasChoices, Field
|
|
@@ -282,7 +282,9 @@ class ManifestPython(BaseModel):
|
|
|
282
282
|
----------
|
|
283
283
|
pip_requirements : Optional[str], default=None
|
|
284
284
|
Path to a requirements.txt file containing (additional) Python
|
|
285
|
-
dependencies that will be bundled with the app.
|
|
285
|
+
dependencies that will be bundled with the app. Alternatively, you can provide a
|
|
286
|
+
list of strings, each representing a package to install, e.g.,
|
|
287
|
+
`["nextmv==0.28.2", "ortools==9.12.4544"]`.
|
|
286
288
|
Aliases: `pip-requirements`.
|
|
287
289
|
model : Optional[ManifestPythonModel], default=None
|
|
288
290
|
Information about an encoded decision model as handled via mlflow. This
|
|
@@ -299,7 +301,7 @@ class ManifestPython(BaseModel):
|
|
|
299
301
|
'requirements.txt'
|
|
300
302
|
"""
|
|
301
303
|
|
|
302
|
-
pip_requirements: Optional[str] = Field(
|
|
304
|
+
pip_requirements: Optional[Union[str, list[str]]] = Field(
|
|
303
305
|
serialization_alias="pip-requirements",
|
|
304
306
|
validation_alias=AliasChoices("pip-requirements", "pip_requirements"),
|
|
305
307
|
default=None,
|
|
@@ -233,11 +233,25 @@ def __install_dependencies(
|
|
|
233
233
|
return
|
|
234
234
|
|
|
235
235
|
pip_requirements = manifest.python.pip_requirements
|
|
236
|
+
|
|
236
237
|
if pip_requirements is None or pip_requirements == "":
|
|
238
|
+
# If no pip requirements are specified, we do not install any dependencies.
|
|
237
239
|
return
|
|
238
240
|
|
|
239
|
-
if
|
|
240
|
-
|
|
241
|
+
if isinstance(pip_requirements, list):
|
|
242
|
+
# If pip_requirements is a list, we write it to a temporary file so that we can
|
|
243
|
+
# pass it to pip.
|
|
244
|
+
pip_requirements_file = os.path.join(temp_dir, "requirements.txt")
|
|
245
|
+
with open(pip_requirements_file, "w") as f:
|
|
246
|
+
for requirement in pip_requirements:
|
|
247
|
+
f.write(requirement + "\n")
|
|
248
|
+
pip_requirements = pip_requirements_file
|
|
249
|
+
elif isinstance(pip_requirements, str):
|
|
250
|
+
# If pip_requirements is a string, we expect it to be a file path to a
|
|
251
|
+
# requirements file.
|
|
252
|
+
pip_requirements = pip_requirements.strip()
|
|
253
|
+
if not os.path.isfile(os.path.join(app_dir, pip_requirements)):
|
|
254
|
+
raise FileNotFoundError(f"pip requirements file '{pip_requirements}' not found in '{app_dir}'")
|
|
241
255
|
|
|
242
256
|
py_cmd = __get_python_command()
|
|
243
257
|
dep_dir = os.path.join(".nextmv", "python", "deps")
|
|
@@ -39,7 +39,6 @@ run_duration(start, end)
|
|
|
39
39
|
Calculate the duration of a run in milliseconds.
|
|
40
40
|
"""
|
|
41
41
|
|
|
42
|
-
import json
|
|
43
42
|
from dataclasses import dataclass
|
|
44
43
|
from datetime import datetime
|
|
45
44
|
from enum import Enum
|
|
@@ -51,6 +50,7 @@ from nextmv.base_model import BaseModel
|
|
|
51
50
|
from nextmv.cloud.status import Status, StatusV2
|
|
52
51
|
from nextmv.input import Input, InputFormat
|
|
53
52
|
from nextmv.output import Output, OutputFormat
|
|
53
|
+
from nextmv.serialization import serialize_json
|
|
54
54
|
|
|
55
55
|
|
|
56
56
|
def run_duration(start: Union[datetime, float], end: Union[datetime, float]) -> int:
|
|
@@ -628,7 +628,7 @@ class TrackedRun:
|
|
|
628
628
|
raise ValueError("Input.input_format must be JSON.")
|
|
629
629
|
elif isinstance(self.input, dict):
|
|
630
630
|
try:
|
|
631
|
-
_ =
|
|
631
|
+
_ = serialize_json(self.input)
|
|
632
632
|
except (TypeError, OverflowError) as e:
|
|
633
633
|
raise ValueError("Input is dict[str, Any] but it is not JSON serializable") from e
|
|
634
634
|
|
|
@@ -637,7 +637,7 @@ class TrackedRun:
|
|
|
637
637
|
raise ValueError("Output.output_format must be JSON.")
|
|
638
638
|
elif isinstance(self.output, dict):
|
|
639
639
|
try:
|
|
640
|
-
_ =
|
|
640
|
+
_ = serialize_json(self.output)
|
|
641
641
|
except (TypeError, OverflowError) as e:
|
|
642
642
|
raise ValueError("Output is dict[str, Any] but it is not JSON serializable") from e
|
|
643
643
|
|
|
@@ -33,6 +33,7 @@ from typing import Any, Optional, Union
|
|
|
33
33
|
|
|
34
34
|
from nextmv.deprecated import deprecated
|
|
35
35
|
from nextmv.options import Options
|
|
36
|
+
from nextmv.serialization import serialize_json
|
|
36
37
|
|
|
37
38
|
|
|
38
39
|
class InputFormat(str, Enum):
|
|
@@ -139,7 +140,7 @@ class Input:
|
|
|
139
140
|
|
|
140
141
|
if self.input_format == InputFormat.JSON:
|
|
141
142
|
try:
|
|
142
|
-
_ =
|
|
143
|
+
_ = serialize_json(self.data)
|
|
143
144
|
except (TypeError, OverflowError) as e:
|
|
144
145
|
raise ValueError(
|
|
145
146
|
f"Input has input_format InputFormat.JSON and "
|
|
@@ -575,7 +575,7 @@ class Options:
|
|
|
575
575
|
|
|
576
576
|
self._parse()
|
|
577
577
|
|
|
578
|
-
def merge(self, new: "Options") -> "Options":
|
|
578
|
+
def merge(self, *new: "Options", skip_parse: bool = False) -> "Options":
|
|
579
579
|
"""
|
|
580
580
|
Merges the current options with the new options.
|
|
581
581
|
|
|
@@ -587,7 +587,11 @@ class Options:
|
|
|
587
587
|
Parameters
|
|
588
588
|
----------
|
|
589
589
|
new : Options
|
|
590
|
-
The new options to merge with the current options.
|
|
590
|
+
The new options to merge with the current options. At least one new option set
|
|
591
|
+
is required to merge. Multiple `Options` instances can be passed.
|
|
592
|
+
skip_parse : bool, optional
|
|
593
|
+
If True, the merged options will not be parsed after merging. This is useful
|
|
594
|
+
if you want to merge further options after this merge. The default is False.
|
|
591
595
|
|
|
592
596
|
Returns
|
|
593
597
|
-------
|
|
@@ -605,11 +609,14 @@ class Options:
|
|
|
605
609
|
--------
|
|
606
610
|
>>> opt1 = Options(Option("duration", str, "30s"))
|
|
607
611
|
>>> opt2 = Options(Option("threads", int, 4))
|
|
608
|
-
>>>
|
|
612
|
+
>>> opt3 = Options(Option("verbose", bool, False))
|
|
613
|
+
>>> merged = opt1.merge(opt2, opt3)
|
|
609
614
|
>>> merged.duration
|
|
610
615
|
'30s'
|
|
611
616
|
>>> merged.threads
|
|
612
617
|
4
|
|
618
|
+
>>> merged.verbose
|
|
619
|
+
False
|
|
613
620
|
"""
|
|
614
621
|
|
|
615
622
|
if self.PARSED:
|
|
@@ -617,14 +624,24 @@ class Options:
|
|
|
617
624
|
"base options have already been parsed, cannot merge. See `Options.parse()` for more information."
|
|
618
625
|
)
|
|
619
626
|
|
|
620
|
-
if new
|
|
621
|
-
raise
|
|
622
|
-
"new options have already been parsed, cannot merge. See `Options.parse()` for more information."
|
|
623
|
-
)
|
|
627
|
+
if not new:
|
|
628
|
+
raise ValueError("at least one new Options instance is required to merge")
|
|
624
629
|
|
|
625
|
-
|
|
630
|
+
for i, opt in enumerate(new):
|
|
631
|
+
if not isinstance(opt, Options):
|
|
632
|
+
raise TypeError(f"expected an <Options> object, but got {type(opt)} in index {i}")
|
|
633
|
+
if opt.PARSED:
|
|
634
|
+
raise RuntimeError(
|
|
635
|
+
f"new options at index {i} have already been parsed, cannot merge. "
|
|
636
|
+
+ "See `Options.parse()` for more information."
|
|
637
|
+
)
|
|
626
638
|
|
|
627
|
-
|
|
639
|
+
# Add the new options to the current options.
|
|
640
|
+
for n in new:
|
|
641
|
+
self.options += n.options
|
|
642
|
+
|
|
643
|
+
if not skip_parse:
|
|
644
|
+
self.parse()
|
|
628
645
|
|
|
629
646
|
return self
|
|
630
647
|
|
|
@@ -42,8 +42,6 @@ write
|
|
|
42
42
|
|
|
43
43
|
import copy
|
|
44
44
|
import csv
|
|
45
|
-
import datetime
|
|
46
|
-
import json
|
|
47
45
|
import os
|
|
48
46
|
import sys
|
|
49
47
|
from dataclasses import dataclass
|
|
@@ -56,6 +54,7 @@ from nextmv.base_model import BaseModel
|
|
|
56
54
|
from nextmv.deprecated import deprecated
|
|
57
55
|
from nextmv.logger import reset_stdout
|
|
58
56
|
from nextmv.options import Options
|
|
57
|
+
from nextmv.serialization import serialize_json
|
|
59
58
|
|
|
60
59
|
|
|
61
60
|
class RunStatistics(BaseModel):
|
|
@@ -644,7 +643,7 @@ class Output:
|
|
|
644
643
|
|
|
645
644
|
if self.output_format == OutputFormat.JSON:
|
|
646
645
|
try:
|
|
647
|
-
_ =
|
|
646
|
+
_ = serialize_json(self.solution)
|
|
648
647
|
except (TypeError, OverflowError) as e:
|
|
649
648
|
raise ValueError(
|
|
650
649
|
f"Output has output_format OutputFormat.JSON and "
|
|
@@ -722,12 +721,6 @@ class Output:
|
|
|
722
721
|
and self.csv_configurations != {}
|
|
723
722
|
):
|
|
724
723
|
output_dict["csv_configurations"] = self.csv_configurations
|
|
725
|
-
elif (
|
|
726
|
-
self.output_format == OutputFormat.JSON
|
|
727
|
-
and self.json_configurations is not None
|
|
728
|
-
and self.json_configurations != {}
|
|
729
|
-
):
|
|
730
|
-
output_dict["json_configurations"] = self.json_configurations
|
|
731
724
|
|
|
732
725
|
return output_dict
|
|
733
726
|
|
|
@@ -822,19 +815,9 @@ class LocalOutputWriter(OutputWriter):
|
|
|
822
815
|
if hasattr(output, "json_configurations") and output.json_configurations is not None:
|
|
823
816
|
json_configurations = output.json_configurations
|
|
824
817
|
|
|
825
|
-
|
|
826
|
-
if "indent" in json_configurations:
|
|
827
|
-
indent = json_configurations["indent"]
|
|
828
|
-
del json_configurations["indent"]
|
|
829
|
-
if "default" in json_configurations:
|
|
830
|
-
custom_serial = json_configurations["default"]
|
|
831
|
-
del json_configurations["default"]
|
|
832
|
-
|
|
833
|
-
serialized = json.dumps(
|
|
818
|
+
serialized = serialize_json(
|
|
834
819
|
output_dict,
|
|
835
|
-
|
|
836
|
-
default=custom_serial,
|
|
837
|
-
**json_configurations,
|
|
820
|
+
json_configurations=json_configurations,
|
|
838
821
|
)
|
|
839
822
|
|
|
840
823
|
if path is None or path == "":
|
|
@@ -877,13 +860,17 @@ class LocalOutputWriter(OutputWriter):
|
|
|
877
860
|
if not os.path.exists(dir_path):
|
|
878
861
|
os.makedirs(dir_path)
|
|
879
862
|
|
|
880
|
-
|
|
863
|
+
json_configurations = {}
|
|
864
|
+
if hasattr(output, "json_configurations") and output.json_configurations is not None:
|
|
865
|
+
json_configurations = output.json_configurations
|
|
866
|
+
|
|
867
|
+
serialized = serialize_json(
|
|
881
868
|
{
|
|
882
869
|
"options": output_dict.get("options", {}),
|
|
883
870
|
"statistics": output_dict.get("statistics", {}),
|
|
884
871
|
"assets": output_dict.get("assets", []),
|
|
885
872
|
},
|
|
886
|
-
|
|
873
|
+
json_configurations=json_configurations,
|
|
887
874
|
)
|
|
888
875
|
print(serialized, file=sys.stdout)
|
|
889
876
|
|
|
@@ -1118,32 +1105,3 @@ def write(
|
|
|
1118
1105
|
"""
|
|
1119
1106
|
|
|
1120
1107
|
writer.write(output, path, skip_stdout_reset)
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
def _custom_serial(obj: Any) -> str:
|
|
1124
|
-
"""
|
|
1125
|
-
JSON serializer for objects not serializable by default json serializer.
|
|
1126
|
-
|
|
1127
|
-
This function provides custom serialization for datetime objects, converting
|
|
1128
|
-
them to ISO format strings.
|
|
1129
|
-
|
|
1130
|
-
Parameters
|
|
1131
|
-
----------
|
|
1132
|
-
obj : Any
|
|
1133
|
-
The object to serialize.
|
|
1134
|
-
|
|
1135
|
-
Returns
|
|
1136
|
-
-------
|
|
1137
|
-
str
|
|
1138
|
-
The serialized representation of the object.
|
|
1139
|
-
|
|
1140
|
-
Raises
|
|
1141
|
-
------
|
|
1142
|
-
TypeError
|
|
1143
|
-
If the object type is not supported for serialization.
|
|
1144
|
-
"""
|
|
1145
|
-
|
|
1146
|
-
if isinstance(obj, (datetime.datetime | datetime.date)):
|
|
1147
|
-
return obj.isoformat()
|
|
1148
|
-
|
|
1149
|
-
raise TypeError(f"Type {type(obj)} not serializable")
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any, Union
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def serialize_json(
|
|
7
|
+
obj: Union[dict, list],
|
|
8
|
+
json_configurations: dict[str, Any] = None,
|
|
9
|
+
) -> str:
|
|
10
|
+
"""
|
|
11
|
+
Serialize a Python object (dict or list) to a JSON string.
|
|
12
|
+
|
|
13
|
+
Parameters
|
|
14
|
+
----------
|
|
15
|
+
obj : Union[dict, list]
|
|
16
|
+
The Python object to serialize.
|
|
17
|
+
json_configurations : dict, optional
|
|
18
|
+
Additional configurations for JSON serialization. This allows customization
|
|
19
|
+
of the Python `json.dumps` function. You can specify parameters like `indent`
|
|
20
|
+
for pretty printing or `default` for custom serialization functions.
|
|
21
|
+
|
|
22
|
+
Returns
|
|
23
|
+
-------
|
|
24
|
+
str
|
|
25
|
+
A JSON string representation of the object.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
# Apply some default configuration if not provided
|
|
29
|
+
json_configurations = json_configurations or {}
|
|
30
|
+
if "default" not in json_configurations:
|
|
31
|
+
json_configurations["default"] = _custom_serial
|
|
32
|
+
if "separators" not in json_configurations:
|
|
33
|
+
json_configurations["separators"] = (",", ":")
|
|
34
|
+
|
|
35
|
+
return json.dumps(
|
|
36
|
+
obj,
|
|
37
|
+
**json_configurations,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _custom_serial(obj: Any) -> str:
|
|
42
|
+
"""
|
|
43
|
+
JSON serializer for objects not serializable by default json serializer.
|
|
44
|
+
|
|
45
|
+
This function provides custom serialization for datetime objects, converting
|
|
46
|
+
them to ISO format strings.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
obj : Any
|
|
51
|
+
The object to serialize.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
str
|
|
56
|
+
The serialized representation of the object.
|
|
57
|
+
|
|
58
|
+
Raises
|
|
59
|
+
------
|
|
60
|
+
TypeError
|
|
61
|
+
If the object type is not supported for serialization.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
if isinstance(obj, (datetime.datetime, datetime.date)):
|
|
65
|
+
return obj.isoformat()
|
|
66
|
+
|
|
67
|
+
raise TypeError(f"Type {type(obj)} not serializable")
|
|
@@ -588,6 +588,69 @@ class TestParameter(unittest.TestCase):
|
|
|
588
588
|
self.assertEqual(opt.foo2, 3)
|
|
589
589
|
self.assertEqual(opt.bar2, 4)
|
|
590
590
|
|
|
591
|
+
def test_merge_lazy(self):
|
|
592
|
+
opt1 = nextmv.Options(
|
|
593
|
+
nextmv.Parameter("foo1", int, default=1),
|
|
594
|
+
nextmv.Parameter("bar1", int, default=2),
|
|
595
|
+
)
|
|
596
|
+
self.assertFalse(opt1.PARSED)
|
|
597
|
+
|
|
598
|
+
opt2 = nextmv.Options(
|
|
599
|
+
nextmv.Parameter("foo2", int, default=3),
|
|
600
|
+
nextmv.Parameter("bar2", int, default=4),
|
|
601
|
+
)
|
|
602
|
+
self.assertFalse(opt2.PARSED)
|
|
603
|
+
|
|
604
|
+
opt3 = nextmv.Options(
|
|
605
|
+
nextmv.Parameter("foo3", int, default=5),
|
|
606
|
+
nextmv.Parameter("bar3", int, default=6),
|
|
607
|
+
)
|
|
608
|
+
self.assertFalse(opt3.PARSED)
|
|
609
|
+
|
|
610
|
+
opt = opt1.merge(opt2, skip_parse=True)
|
|
611
|
+
self.assertFalse(opt.PARSED)
|
|
612
|
+
|
|
613
|
+
opt = opt.merge(opt3, skip_parse=True)
|
|
614
|
+
self.assertFalse(opt.PARSED)
|
|
615
|
+
|
|
616
|
+
opt.parse()
|
|
617
|
+
self.assertTrue(opt.PARSED)
|
|
618
|
+
|
|
619
|
+
self.assertEqual(opt.foo1, 1)
|
|
620
|
+
self.assertEqual(opt.bar1, 2)
|
|
621
|
+
self.assertEqual(opt.foo2, 3)
|
|
622
|
+
self.assertEqual(opt.bar2, 4)
|
|
623
|
+
self.assertEqual(opt.foo3, 5)
|
|
624
|
+
self.assertEqual(opt.bar3, 6)
|
|
625
|
+
|
|
626
|
+
def test_merge_multi(self):
|
|
627
|
+
opt1 = nextmv.Options(
|
|
628
|
+
nextmv.Parameter("foo1", int, default=1),
|
|
629
|
+
nextmv.Parameter("bar1", int, default=2),
|
|
630
|
+
)
|
|
631
|
+
self.assertFalse(opt1.PARSED)
|
|
632
|
+
|
|
633
|
+
opt2 = nextmv.Options(
|
|
634
|
+
nextmv.Parameter("foo2", int, default=3),
|
|
635
|
+
nextmv.Parameter("bar2", int, default=4),
|
|
636
|
+
)
|
|
637
|
+
self.assertFalse(opt2.PARSED)
|
|
638
|
+
|
|
639
|
+
opt3 = nextmv.Options(
|
|
640
|
+
nextmv.Parameter("foo3", int, default=5),
|
|
641
|
+
nextmv.Parameter("bar3", int, default=6),
|
|
642
|
+
)
|
|
643
|
+
self.assertFalse(opt3.PARSED)
|
|
644
|
+
|
|
645
|
+
opt1.merge(opt2, opt3)
|
|
646
|
+
self.assertTrue(opt1.PARSED)
|
|
647
|
+
self.assertEqual(opt1.foo1, 1)
|
|
648
|
+
self.assertEqual(opt1.bar1, 2)
|
|
649
|
+
self.assertEqual(opt1.foo2, 3)
|
|
650
|
+
self.assertEqual(opt1.bar2, 4)
|
|
651
|
+
self.assertEqual(opt1.foo3, 5)
|
|
652
|
+
self.assertEqual(opt1.bar3, 6)
|
|
653
|
+
|
|
591
654
|
def test_cant_merge(self):
|
|
592
655
|
opt1 = nextmv.Options(
|
|
593
656
|
nextmv.Parameter("foo1", int, default=1),
|
|
@@ -118,13 +118,6 @@ class TestOutput(unittest.TestCase):
|
|
|
118
118
|
result = output.to_dict()
|
|
119
119
|
self.assertEqual(result["assets"][0]["name"], "asset3")
|
|
120
120
|
|
|
121
|
-
# Test with JSON configurations
|
|
122
|
-
json_config = {"indent": 4, "sort_keys": True}
|
|
123
|
-
output = nextmv.Output(output_format=nextmv.OutputFormat.JSON, json_configurations=json_config)
|
|
124
|
-
result = output.to_dict()
|
|
125
|
-
self.assertEqual(result["json_configurations"]["indent"], 4)
|
|
126
|
-
self.assertEqual(result["json_configurations"]["sort_keys"], True)
|
|
127
|
-
|
|
128
121
|
# Test with CSV configurations
|
|
129
122
|
csv_config = {"delimiter": ";", "quoting": csv.QUOTE_NONNUMERIC}
|
|
130
123
|
output = nextmv.Output(output_format=nextmv.OutputFormat.CSV_ARCHIVE, csv_configurations=csv_config)
|
|
@@ -183,7 +176,6 @@ class TestOutput(unittest.TestCase):
|
|
|
183
176
|
self.assertEqual(result["assets"][0]["name"], "asset1")
|
|
184
177
|
self.assertEqual(result["assets"][0]["visual"]["schema"], "chartjs")
|
|
185
178
|
self.assertEqual(result["solution"]["value"], 42)
|
|
186
|
-
self.assertEqual(result["json_configurations"]["indent"], 4)
|
|
187
179
|
|
|
188
180
|
def test_local_writer_json_stdout_default(self):
|
|
189
181
|
output = nextmv.Output(
|
|
@@ -262,7 +254,7 @@ class TestOutput(unittest.TestCase):
|
|
|
262
254
|
|
|
263
255
|
self.assertEqual(
|
|
264
256
|
mock_stdout.getvalue(),
|
|
265
|
-
'{"assets":[],"
|
|
257
|
+
'{"assets":[],"options":{},"solution":{"empanadas":"are_life"},"statistics":{"foo":"bar"}}\n',
|
|
266
258
|
)
|
|
267
259
|
|
|
268
260
|
def test_local_writer_json_stdout_with_options(self):
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import json
|
|
3
|
+
import unittest
|
|
4
|
+
|
|
5
|
+
import nextmv.serialization
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class TestSerialization(unittest.TestCase):
|
|
9
|
+
"""Tests for the common serialization functionality."""
|
|
10
|
+
|
|
11
|
+
def test_default_serialization(self):
|
|
12
|
+
"""Test the default serialization"""
|
|
13
|
+
|
|
14
|
+
data = {
|
|
15
|
+
"name": "Test",
|
|
16
|
+
"value": 42,
|
|
17
|
+
"timestamp": nextmv.serialization._custom_serial(datetime.datetime(2023, 10, 1)),
|
|
18
|
+
}
|
|
19
|
+
serialized = nextmv.serialization.serialize_json(data)
|
|
20
|
+
expected = json.dumps(
|
|
21
|
+
{
|
|
22
|
+
"name": "Test",
|
|
23
|
+
"value": 42,
|
|
24
|
+
"timestamp": "2023-10-01T00:00:00",
|
|
25
|
+
},
|
|
26
|
+
separators=(",", ":"),
|
|
27
|
+
)
|
|
28
|
+
self.assertEqual(serialized, expected)
|
|
29
|
+
|
|
30
|
+
def test_custom_serialization(self):
|
|
31
|
+
"""Test custom serialization with additional configurations"""
|
|
32
|
+
|
|
33
|
+
data = {
|
|
34
|
+
"name": "Test",
|
|
35
|
+
"value": 42,
|
|
36
|
+
"timestamp": nextmv.serialization._custom_serial(datetime.datetime(2023, 10, 1)),
|
|
37
|
+
}
|
|
38
|
+
json_configurations = {
|
|
39
|
+
"indent": 2,
|
|
40
|
+
"default": nextmv.serialization._custom_serial,
|
|
41
|
+
"separators": (",", ": "),
|
|
42
|
+
}
|
|
43
|
+
serialized = nextmv.serialization.serialize_json(data, json_configurations)
|
|
44
|
+
expected = json.dumps(
|
|
45
|
+
{
|
|
46
|
+
"name": "Test",
|
|
47
|
+
"value": 42,
|
|
48
|
+
"timestamp": "2023-10-01T00:00:00",
|
|
49
|
+
},
|
|
50
|
+
indent=2,
|
|
51
|
+
separators=(",", ": "),
|
|
52
|
+
)
|
|
53
|
+
self.assertEqual(serialized, expected)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "v0.28.1.dev0"
|
|
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
|
|
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
|
|
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
|