nextmv 0.21.1__tar.gz → 0.22.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.
- {nextmv-0.21.1 → nextmv-0.22.0}/PKG-INFO +1 -1
- nextmv-0.22.0/nextmv/__about__.py +1 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/cloud/application.py +187 -64
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/options.py +27 -0
- nextmv-0.22.0/tests/cloud/test_application.py +48 -0
- nextmv-0.21.1/nextmv/__about__.py +0 -1
- nextmv-0.21.1/tests/cloud/test_application.py +0 -23
- {nextmv-0.21.1 → nextmv-0.22.0}/.gitignore +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/LICENSE +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/README.md +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/__entrypoint__.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/__init__.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/base_model.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/cloud/__init__.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/cloud/acceptance_test.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/cloud/account.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/cloud/batch_experiment.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/cloud/client.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/cloud/input_set.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/cloud/instance.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/cloud/manifest.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/cloud/package.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/cloud/run.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/cloud/secrets.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/cloud/status.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/cloud/version.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/input.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/logger.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/model.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/nextmv/output.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/pyproject.toml +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/requirements.txt +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/__init__.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/cloud/__init__.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/cloud/app.yaml +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/cloud/test_client.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/cloud/test_manifest.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/cloud/test_package.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/scripts/__init__.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/scripts/options1.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/scripts/options2.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/scripts/options3.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/scripts/options4.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/scripts/options5.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/scripts/options6.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/scripts/options7.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/test_base_model.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/test_entrypoint/__init__.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/test_entrypoint/test_entrypoint.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/test_input.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/test_logger.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/test_model.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/test_options.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/test_output.py +0 -0
- {nextmv-0.21.1 → nextmv-0.22.0}/tests/test_version.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "v0.22.0"
|
|
@@ -4,6 +4,7 @@ import json
|
|
|
4
4
|
import random
|
|
5
5
|
import shutil
|
|
6
6
|
import time
|
|
7
|
+
from collections.abc import Callable
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from datetime import datetime
|
|
9
10
|
from typing import Any, Optional, Union
|
|
@@ -22,9 +23,10 @@ from nextmv.cloud.run import ExternalRunResult, RunConfiguration, RunInformation
|
|
|
22
23
|
from nextmv.cloud.secrets import Secret, SecretsCollection, SecretsCollectionSummary
|
|
23
24
|
from nextmv.cloud.status import StatusV2
|
|
24
25
|
from nextmv.cloud.version import Version
|
|
25
|
-
from nextmv.input import Input
|
|
26
|
+
from nextmv.input import Input, InputFormat
|
|
26
27
|
from nextmv.logger import log
|
|
27
28
|
from nextmv.model import Model, ModelConfiguration
|
|
29
|
+
from nextmv.options import Options
|
|
28
30
|
from nextmv.output import Output
|
|
29
31
|
|
|
30
32
|
_MAX_RUN_SIZE: int = 5 * 1024 * 1024
|
|
@@ -39,7 +41,8 @@ class DownloadURL(BaseModel):
|
|
|
39
41
|
"""URL to use for downloading the file."""
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
|
|
44
|
+
@dataclass
|
|
45
|
+
class PollingOptions:
|
|
43
46
|
"""
|
|
44
47
|
Options to use when polling for a run result.
|
|
45
48
|
|
|
@@ -90,6 +93,13 @@ class PollingOptions(BaseModel):
|
|
|
90
93
|
"""
|
|
91
94
|
verbose: bool = False
|
|
92
95
|
"""Whether to log the polling strategy. This is useful for debugging."""
|
|
96
|
+
stop: Optional[Callable[[], bool]] = None
|
|
97
|
+
"""
|
|
98
|
+
Function to call to check if the polling should stop. This is useful for
|
|
99
|
+
stopping the polling based on external conditions. The function should
|
|
100
|
+
return True to stop the polling and False to continue. The function does
|
|
101
|
+
not receive any arguments. The function is called before each poll.
|
|
102
|
+
"""
|
|
93
103
|
|
|
94
104
|
|
|
95
105
|
_DEFAULT_POLLING_OPTIONS: PollingOptions = PollingOptions()
|
|
@@ -784,84 +794,144 @@ class Application:
|
|
|
784
794
|
|
|
785
795
|
return Instance.from_dict(response.json())
|
|
786
796
|
|
|
787
|
-
def new_run( # noqa: C901 #
|
|
797
|
+
def new_run( # noqa: C901 # Refactor this function at some point.
|
|
788
798
|
self,
|
|
789
|
-
input: Union[dict[str, Any], BaseModel, str] = None,
|
|
799
|
+
input: Union[Input, dict[str, Any], BaseModel, str] = None,
|
|
790
800
|
instance_id: Optional[str] = None,
|
|
791
801
|
name: Optional[str] = None,
|
|
792
802
|
description: Optional[str] = None,
|
|
793
803
|
upload_id: Optional[str] = None,
|
|
794
|
-
options: Optional[dict[str, str]] = None,
|
|
795
|
-
configuration: Optional[RunConfiguration] = None,
|
|
804
|
+
options: Optional[Union[Options, dict[str, str]]] = None,
|
|
805
|
+
configuration: Optional[Union[RunConfiguration, dict[str, any]]] = None,
|
|
796
806
|
batch_experiment_id: Optional[str] = None,
|
|
797
|
-
external_result: Optional[ExternalRunResult] = None,
|
|
807
|
+
external_result: Optional[Union[ExternalRunResult, dict[str, any]]] = None,
|
|
798
808
|
) -> str:
|
|
799
809
|
"""
|
|
800
810
|
Submit an input to start a new run of the application. Returns the
|
|
801
|
-
run_id of the submitted run.
|
|
811
|
+
`run_id` of the submitted run.
|
|
802
812
|
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
813
|
+
Parameters
|
|
814
|
+
----------
|
|
815
|
+
input: Union[Input, dict[str, Any], BaseModel, str]
|
|
816
|
+
Input to use for the run. This can be a `nextmv.Input` object,
|
|
817
|
+
`dict`, `BaseModel` or `str`. If `nextmv.Input` is used, then the
|
|
818
|
+
input is extracted from the `.data` property. Note that for now,
|
|
819
|
+
`InputFormat.CSV_ARCHIVE` is not supported as an
|
|
820
|
+
`input.input_format`. If an input is too large, it will be uploaded
|
|
821
|
+
with the `upload_large_input` method.
|
|
822
|
+
instance_id: Optional[str]
|
|
823
|
+
ID of the instance to use for the run. If not provided, the default
|
|
824
|
+
instance ID associated to the Class (`default_instance_id`) is
|
|
825
|
+
used.
|
|
826
|
+
name: Optional[str]
|
|
827
|
+
Name of the run.
|
|
828
|
+
description: Optional[str]
|
|
829
|
+
Description of the run.
|
|
830
|
+
upload_id: Optional[str]
|
|
831
|
+
ID to use when running a large input. If the `input` exceeds the
|
|
832
|
+
maximum allowed size, then it is uploaded and the corresponding
|
|
833
|
+
`upload_id` is used.
|
|
834
|
+
options: Optional[Union[Options, dict[str, str]]]
|
|
835
|
+
Options to use for the run. This can be a `nextmv.Options` object
|
|
836
|
+
or a dict. If a dict is used, the keys must be strings and the
|
|
837
|
+
values must be strings as well. If a `nextmv.Options` object is
|
|
838
|
+
used, the options are extracted from the `.to_cloud_dict()` method.
|
|
839
|
+
Note that specifying `options` overrides the `input.options` (if
|
|
840
|
+
the `input` is of type `nextmv.Input`).
|
|
841
|
+
configuration: Optional[Union[RunConfiguration, dict[str, any]]]
|
|
842
|
+
Configuration to use for the run. This can be a
|
|
843
|
+
`cloud.RunConfiguration` object or a dict. If the object is used,
|
|
844
|
+
then the `.to_dict()` method is applied to extract the
|
|
845
|
+
configuration.
|
|
846
|
+
batch_experiment_id: Optional[str]
|
|
847
|
+
ID of a batch experiment to associate the run with. This is used
|
|
848
|
+
when the run is part of a batch experiment.
|
|
849
|
+
external_result: Optional[Union[ExternalRunResult, dict[str, any]]]
|
|
850
|
+
External result to use for the run. This can be a
|
|
851
|
+
`cloud.ExternalRunResult` object or a dict. If the object is used,
|
|
852
|
+
then the `.to_dict()` method is applied to extract the
|
|
853
|
+
configuration. This is used when the run is an external run. We
|
|
854
|
+
suggest that instead of specifying this parameter, you use the
|
|
855
|
+
`track_run` method of the class.
|
|
817
856
|
|
|
818
|
-
Returns
|
|
819
|
-
|
|
857
|
+
Returns
|
|
858
|
+
----------
|
|
859
|
+
str
|
|
860
|
+
ID (`run_id`) of the run that was submitted.
|
|
820
861
|
|
|
821
|
-
Raises
|
|
862
|
+
Raises
|
|
863
|
+
----------
|
|
822
864
|
requests.HTTPError: If the response status code is not 2xx.
|
|
865
|
+
ValueError:
|
|
866
|
+
If the `input` is of type `nextmv.Input` and the
|
|
867
|
+
`.input_format` is not `JSON`. If the final `options` are not
|
|
868
|
+
of type `dict[str,str]`.
|
|
823
869
|
"""
|
|
824
870
|
|
|
825
|
-
|
|
871
|
+
input_data = None
|
|
826
872
|
if isinstance(input, BaseModel):
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
elif isinstance(input,
|
|
831
|
-
|
|
873
|
+
input_data = input.to_dict()
|
|
874
|
+
elif isinstance(input, dict) or isinstance(input, str):
|
|
875
|
+
input_data = input
|
|
876
|
+
elif isinstance(input, Input):
|
|
877
|
+
if input.input_format == InputFormat.CSV_ARCHIVE:
|
|
878
|
+
raise ValueError("csv-archive is not supported")
|
|
879
|
+
input_data = input.data
|
|
832
880
|
|
|
833
|
-
|
|
881
|
+
input_size = 0
|
|
882
|
+
if input_data is not None:
|
|
883
|
+
input_size = get_size(input_data)
|
|
834
884
|
|
|
885
|
+
upload_url_required = input_size > _MAX_RUN_SIZE
|
|
835
886
|
upload_id_used = upload_id is not None
|
|
887
|
+
|
|
836
888
|
if not upload_id_used and upload_url_required:
|
|
837
889
|
upload_url = self.upload_url()
|
|
838
|
-
self.upload_large_input(input=
|
|
890
|
+
self.upload_large_input(input=input_data, upload_url=upload_url)
|
|
839
891
|
upload_id = upload_url.upload_id
|
|
840
892
|
upload_id_used = True
|
|
841
893
|
|
|
894
|
+
options_dict = {}
|
|
895
|
+
if isinstance(input, Input) and input.options is not None:
|
|
896
|
+
options_dict = input.options.to_cloud_dict()
|
|
897
|
+
|
|
842
898
|
if options is not None:
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
899
|
+
if isinstance(options, Options):
|
|
900
|
+
options_dict = options.to_cloud_dict()
|
|
901
|
+
elif isinstance(options, dict):
|
|
902
|
+
for k, v in options.items():
|
|
903
|
+
if isinstance(v, str):
|
|
904
|
+
options_dict[k] = v
|
|
905
|
+
else:
|
|
906
|
+
options_dict[k] = json.dumps(v)
|
|
846
907
|
|
|
847
908
|
payload = {}
|
|
848
909
|
if upload_id_used:
|
|
849
910
|
payload["upload_id"] = upload_id
|
|
850
911
|
else:
|
|
851
|
-
payload["input"] =
|
|
912
|
+
payload["input"] = input_data
|
|
852
913
|
|
|
853
914
|
if name is not None:
|
|
854
915
|
payload["name"] = name
|
|
855
916
|
if description is not None:
|
|
856
917
|
payload["description"] = description
|
|
857
|
-
if
|
|
858
|
-
|
|
918
|
+
if len(options_dict) > 0:
|
|
919
|
+
for k, v in options_dict.items():
|
|
920
|
+
if not isinstance(v, str):
|
|
921
|
+
raise ValueError(f"options must be dict[str,str], option {k} has type {type(v)} instead.")
|
|
922
|
+
payload["options"] = options_dict
|
|
859
923
|
if configuration is not None:
|
|
860
|
-
|
|
924
|
+
configuration_dict = (
|
|
925
|
+
configuration.to_dict() if isinstance(configuration, RunConfiguration) else configuration
|
|
926
|
+
)
|
|
927
|
+
payload["configuration"] = configuration_dict
|
|
861
928
|
if batch_experiment_id is not None:
|
|
862
929
|
payload["batch_experiment_id"] = batch_experiment_id
|
|
863
930
|
if external_result is not None:
|
|
864
|
-
|
|
931
|
+
external_dict = (
|
|
932
|
+
external_result.to_dict() if isinstance(external_result, ExternalRunResult) else external_result
|
|
933
|
+
)
|
|
934
|
+
payload["result"] = external_dict
|
|
865
935
|
|
|
866
936
|
query_params = {
|
|
867
937
|
"instance_id": instance_id if instance_id is not None else self.default_instance_id,
|
|
@@ -877,42 +947,83 @@ class Application:
|
|
|
877
947
|
|
|
878
948
|
def new_run_with_result(
|
|
879
949
|
self,
|
|
880
|
-
input: Union[dict[str, Any], BaseModel] = None,
|
|
950
|
+
input: Union[Input, dict[str, Any], BaseModel, str] = None,
|
|
881
951
|
instance_id: Optional[str] = None,
|
|
882
952
|
name: Optional[str] = None,
|
|
883
953
|
description: Optional[str] = None,
|
|
884
954
|
upload_id: Optional[str] = None,
|
|
885
|
-
run_options: Optional[dict[str, str]] = None,
|
|
955
|
+
run_options: Optional[Union[Options, dict[str, str]]] = None,
|
|
886
956
|
polling_options: PollingOptions = _DEFAULT_POLLING_OPTIONS,
|
|
887
|
-
configuration: Optional[RunConfiguration] = None,
|
|
957
|
+
configuration: Optional[Union[RunConfiguration, dict[str, any]]] = None,
|
|
888
958
|
batch_experiment_id: Optional[str] = None,
|
|
889
|
-
external_result: Optional[ExternalRunResult] = None,
|
|
959
|
+
external_result: Optional[Union[ExternalRunResult, dict[str, any]]] = None,
|
|
890
960
|
) -> RunResult:
|
|
891
961
|
"""
|
|
892
962
|
Submit an input to start a new run of the application and poll for the
|
|
893
|
-
result. This is a convenience method that combines the new_run and
|
|
894
|
-
run_result_with_polling methods, applying polling logic to check when
|
|
963
|
+
result. This is a convenience method that combines the `new_run` and
|
|
964
|
+
`run_result_with_polling` methods, applying polling logic to check when
|
|
895
965
|
the run succeeded.
|
|
896
966
|
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
967
|
+
Parameters
|
|
968
|
+
----------
|
|
969
|
+
input: Union[Input, dict[str, Any], BaseModel, str]
|
|
970
|
+
Input to use for the run. This can be a `nextmv.Input` object,
|
|
971
|
+
`dict`, `BaseModel` or `str`. If `nextmv.Input` is used, then the
|
|
972
|
+
input is extracted from the `.data` property. Note that for now,
|
|
973
|
+
`InputFormat.CSV_ARCHIVE` is not supported as an
|
|
974
|
+
`input.input_format`. If an input is too large, it will be uploaded
|
|
975
|
+
with the `upload_large_input` method.
|
|
976
|
+
instance_id: Optional[str]
|
|
977
|
+
ID of the instance to use for the run. If not provided, the default
|
|
978
|
+
instance ID associated to the Class (`default_instance_id`) is
|
|
979
|
+
used.
|
|
980
|
+
name: Optional[str]
|
|
981
|
+
Name of the run.
|
|
982
|
+
description: Optional[str]
|
|
983
|
+
Description of the run.
|
|
984
|
+
upload_id: Optional[str]
|
|
985
|
+
ID to use when running a large input. If the `input` exceeds the
|
|
986
|
+
maximum allowed size, then it is uploaded and the corresponding
|
|
987
|
+
`upload_id` is used.
|
|
988
|
+
run_options: Optional[Union[Options, dict[str, str]]]
|
|
989
|
+
Options to use for the run. This can be a `nextmv.Options` object
|
|
990
|
+
or a dict. If a dict is used, the keys must be strings and the
|
|
991
|
+
values must be strings as well. If a `nextmv.Options` object is
|
|
992
|
+
used, the options are extracted from the `.to_cloud_dict()` method.
|
|
993
|
+
Note that specifying `options` overrides the `input.options` (if
|
|
994
|
+
the `input` is of type `nextmv.Input`).
|
|
995
|
+
polling_options: PollingOptions
|
|
996
|
+
Options to use when polling for the run result. This is a
|
|
997
|
+
convenience method that combines the `new_run` and
|
|
998
|
+
`run_result_with_polling` methods, applying polling logic to check
|
|
999
|
+
when the run succeeded.
|
|
1000
|
+
configuration: Optional[Union[RunConfiguration, dict[str, any]]]
|
|
1001
|
+
Configuration to use for the run. This can be a
|
|
1002
|
+
`cloud.RunConfiguration` object or a dict. If the object is used,
|
|
1003
|
+
then the `.to_dict()` method is applied to extract the
|
|
1004
|
+
configuration.
|
|
1005
|
+
batch_experiment_id: Optional[str]
|
|
1006
|
+
ID of a batch experiment to associate the run with. This is used
|
|
1007
|
+
when the run is part of a batch experiment.
|
|
1008
|
+
external_result: Optional[Union[ExternalRunResult, dict[str, any]]]
|
|
1009
|
+
External result to use for the run. This can be a
|
|
1010
|
+
`cloud.ExternalRunResult` object or a dict. If the object is used,
|
|
1011
|
+
then the `.to_dict()` method is applied to extract the
|
|
1012
|
+
configuration. This is used when the run is an external run. We
|
|
1013
|
+
suggest that instead of specifying this parameter, you use the
|
|
1014
|
+
`track_run_with_result` method of the class.
|
|
911
1015
|
|
|
912
|
-
|
|
1016
|
+
Returns
|
|
1017
|
+
----------
|
|
1018
|
+
RunResult
|
|
913
1019
|
Result of the run.
|
|
914
1020
|
|
|
915
|
-
Raises
|
|
1021
|
+
Raises
|
|
1022
|
+
----------
|
|
1023
|
+
ValueError:
|
|
1024
|
+
If the `input` is of type `nextmv.Input` and the
|
|
1025
|
+
`.input_format` is not `JSON`.
|
|
1026
|
+
If the final `options` are not of type `dict[str,str]`.
|
|
916
1027
|
requests.HTTPError: If the response status code is not 2xx.
|
|
917
1028
|
TimeoutError: If the run does not succeed after the polling
|
|
918
1029
|
strategy is exhausted based on time duration.
|
|
@@ -1656,7 +1767,7 @@ class Application:
|
|
|
1656
1767
|
)
|
|
1657
1768
|
|
|
1658
1769
|
|
|
1659
|
-
def poll(polling_options: PollingOptions, polling_func:
|
|
1770
|
+
def poll(polling_options: PollingOptions, polling_func: Callable[[], tuple[any, bool]]) -> any:
|
|
1660
1771
|
"""
|
|
1661
1772
|
Auxiliary function for polling.
|
|
1662
1773
|
|
|
@@ -1689,9 +1800,16 @@ def poll(polling_options: PollingOptions, polling_func: callable) -> any:
|
|
|
1689
1800
|
time.sleep(polling_options.initial_delay)
|
|
1690
1801
|
|
|
1691
1802
|
start_time = time.time()
|
|
1803
|
+
stopped = False
|
|
1692
1804
|
|
|
1693
1805
|
# Begin the polling process.
|
|
1694
1806
|
for ix in range(polling_options.max_tries):
|
|
1807
|
+
# Check is we should stop polling according to the stop callback.
|
|
1808
|
+
if polling_options.stop is not None and polling_options.stop():
|
|
1809
|
+
stopped = True
|
|
1810
|
+
|
|
1811
|
+
break
|
|
1812
|
+
|
|
1695
1813
|
# We check if we can stop polling.
|
|
1696
1814
|
result, ok = polling_func()
|
|
1697
1815
|
if polling_options.verbose:
|
|
@@ -1722,6 +1840,11 @@ def poll(polling_options: PollingOptions, polling_func: callable) -> any:
|
|
|
1722
1840
|
|
|
1723
1841
|
time.sleep(sleep_duration)
|
|
1724
1842
|
|
|
1843
|
+
if stopped:
|
|
1844
|
+
log("polling | stop condition met, stopping polling")
|
|
1845
|
+
|
|
1846
|
+
return None
|
|
1847
|
+
|
|
1725
1848
|
raise RuntimeError(
|
|
1726
1849
|
f"polling did not succeed after {polling_options.max_tries} tries",
|
|
1727
1850
|
)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import argparse
|
|
4
4
|
import builtins
|
|
5
5
|
import copy
|
|
6
|
+
import json
|
|
6
7
|
import os
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from typing import Any, Optional
|
|
@@ -198,6 +199,32 @@ class Options:
|
|
|
198
199
|
|
|
199
200
|
return m.to_dict()["config"]
|
|
200
201
|
|
|
202
|
+
def to_cloud_dict(self) -> dict[str, str]:
|
|
203
|
+
"""
|
|
204
|
+
Converts the options to a dict that can be used in the Nextmv Cloud.
|
|
205
|
+
Cloud has a hard requirement that options are passed as strings. This
|
|
206
|
+
method converts the options to a dict with string values. This is
|
|
207
|
+
useful for passing options to the Nextmv Cloud.
|
|
208
|
+
As a side effect, this method parses the options if they have not been
|
|
209
|
+
parsed yet. See the `parse` method for more information.
|
|
210
|
+
|
|
211
|
+
Returns
|
|
212
|
+
-------
|
|
213
|
+
dict[str, str]
|
|
214
|
+
The options as a dict with string values.
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
options_dict = self.to_dict()
|
|
218
|
+
|
|
219
|
+
cloud_dict = {}
|
|
220
|
+
for k, v in options_dict.items():
|
|
221
|
+
if isinstance(v, str):
|
|
222
|
+
cloud_dict[k] = v
|
|
223
|
+
else:
|
|
224
|
+
cloud_dict[k] = json.dumps(v)
|
|
225
|
+
|
|
226
|
+
return cloud_dict
|
|
227
|
+
|
|
201
228
|
def parameters_dict(self) -> list[dict[str, Any]]:
|
|
202
229
|
"""
|
|
203
230
|
Converts the options to a list of dicts. Each dict is the dict
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from nextmv.cloud.application import PollingOptions, poll
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TestApplication(unittest.TestCase):
|
|
7
|
+
def test_poll(self):
|
|
8
|
+
counter = 0
|
|
9
|
+
|
|
10
|
+
def polling_func() -> tuple[any, bool]:
|
|
11
|
+
nonlocal counter
|
|
12
|
+
counter += 1
|
|
13
|
+
|
|
14
|
+
if counter < 4:
|
|
15
|
+
return "result", False
|
|
16
|
+
|
|
17
|
+
return "result", True
|
|
18
|
+
|
|
19
|
+
polling_options = PollingOptions(verbose=True)
|
|
20
|
+
|
|
21
|
+
result = poll(polling_options, polling_func)
|
|
22
|
+
|
|
23
|
+
self.assertEqual(result, "result")
|
|
24
|
+
|
|
25
|
+
def test_poll_stop_callback(self):
|
|
26
|
+
counter = 0
|
|
27
|
+
|
|
28
|
+
# The polling func would stop after 9 calls.
|
|
29
|
+
def polling_func() -> tuple[any, bool]:
|
|
30
|
+
nonlocal counter
|
|
31
|
+
counter += 1
|
|
32
|
+
|
|
33
|
+
if counter < 10:
|
|
34
|
+
return "result", False
|
|
35
|
+
|
|
36
|
+
return "result", True
|
|
37
|
+
|
|
38
|
+
# The stop callback makes sure that the polling stops sooner, after 3
|
|
39
|
+
# calls.
|
|
40
|
+
def stop() -> bool:
|
|
41
|
+
if counter == 3:
|
|
42
|
+
return True
|
|
43
|
+
|
|
44
|
+
polling_options = PollingOptions(verbose=True, stop=stop)
|
|
45
|
+
|
|
46
|
+
result = poll(polling_options, polling_func)
|
|
47
|
+
|
|
48
|
+
self.assertIsNone(result)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "v0.21.1"
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import unittest
|
|
2
|
-
|
|
3
|
-
from nextmv.cloud.application import PollingOptions, poll
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class TestApplication(unittest.TestCase):
|
|
7
|
-
def test_poll(self):
|
|
8
|
-
counter = 0
|
|
9
|
-
|
|
10
|
-
def polling_func() -> tuple[any, bool]:
|
|
11
|
-
nonlocal counter
|
|
12
|
-
counter += 1
|
|
13
|
-
|
|
14
|
-
if counter < 4:
|
|
15
|
-
return "result", False
|
|
16
|
-
|
|
17
|
-
return "result", True
|
|
18
|
-
|
|
19
|
-
polling_options = PollingOptions(verbose=True)
|
|
20
|
-
|
|
21
|
-
result = poll(polling_options, polling_func)
|
|
22
|
-
|
|
23
|
-
self.assertEqual(result, "result")
|
|
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
|