nextmv 0.21.0__py3-none-any.whl → 0.22.0__py3-none-any.whl

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/__about__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "v0.21.0"
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
- class PollingOptions(BaseModel):
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()
@@ -443,6 +453,7 @@ class Application:
443
453
  id: Optional[str] = None,
444
454
  description: Optional[str] = None,
445
455
  is_workflow: Optional[bool] = None,
456
+ exist_ok: bool = False,
446
457
  ) -> "Application":
447
458
  """
448
459
  Create a new application.
@@ -458,6 +469,9 @@ class Application:
458
469
  The new application.
459
470
  """
460
471
 
472
+ if exist_ok and cls.exists(client=client, id=id):
473
+ return Application(client=client, id=id)
474
+
461
475
  payload = {
462
476
  "name": name,
463
477
  }
@@ -780,84 +794,144 @@ class Application:
780
794
 
781
795
  return Instance.from_dict(response.json())
782
796
 
783
- def new_run( # noqa: C901 # Lot of if statements, but clear logic.
797
+ def new_run( # noqa: C901 # Refactor this function at some point.
784
798
  self,
785
- input: Union[dict[str, Any], BaseModel, str] = None,
799
+ input: Union[Input, dict[str, Any], BaseModel, str] = None,
786
800
  instance_id: Optional[str] = None,
787
801
  name: Optional[str] = None,
788
802
  description: Optional[str] = None,
789
803
  upload_id: Optional[str] = None,
790
- options: Optional[dict[str, str]] = None,
791
- configuration: Optional[RunConfiguration] = None,
804
+ options: Optional[Union[Options, dict[str, str]]] = None,
805
+ configuration: Optional[Union[RunConfiguration, dict[str, any]]] = None,
792
806
  batch_experiment_id: Optional[str] = None,
793
- external_result: Optional[ExternalRunResult] = None,
807
+ external_result: Optional[Union[ExternalRunResult, dict[str, any]]] = None,
794
808
  ) -> str:
795
809
  """
796
810
  Submit an input to start a new run of the application. Returns the
797
- run_id of the submitted run.
811
+ `run_id` of the submitted run.
798
812
 
799
- Args:
800
- input: Input to use for the run. This can be JSON (given as dict
801
- or BaseModel) or text (given as str).
802
- instance_id: ID of the instance to use for the run. If not
803
- provided, the default_instance_id will be used.
804
- name: Name of the run.
805
- description: Description of the run.
806
- upload_id: ID to use when running a large input.
807
- options: Options to use for the run.
808
- configuration: Configuration to use for the run.
809
- batch_experiment_id: ID of a batch experiment to associate the run
810
- with.
811
- external_result: External result to use for the run, if this is an
812
- external run.
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.
813
856
 
814
- Returns:
815
- ID of the submitted run.
857
+ Returns
858
+ ----------
859
+ str
860
+ ID (`run_id`) of the run that was submitted.
816
861
 
817
- Raises:
862
+ Raises
863
+ ----------
818
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]`.
819
869
  """
820
870
 
821
- input_size = 0
871
+ input_data = None
822
872
  if isinstance(input, BaseModel):
823
- input = input.to_dict()
824
- if input is not None:
825
- input_size = get_size(input)
826
- elif isinstance(input, dict):
827
- input_size = get_size(input)
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
828
880
 
829
- upload_url_required = isinstance(input, str) or input_size > _MAX_RUN_SIZE
881
+ input_size = 0
882
+ if input_data is not None:
883
+ input_size = get_size(input_data)
830
884
 
885
+ upload_url_required = input_size > _MAX_RUN_SIZE
831
886
  upload_id_used = upload_id is not None
887
+
832
888
  if not upload_id_used and upload_url_required:
833
889
  upload_url = self.upload_url()
834
- self.upload_large_input(input=input, upload_url=upload_url)
890
+ self.upload_large_input(input=input_data, upload_url=upload_url)
835
891
  upload_id = upload_url.upload_id
836
892
  upload_id_used = True
837
893
 
894
+ options_dict = {}
895
+ if isinstance(input, Input) and input.options is not None:
896
+ options_dict = input.options.to_cloud_dict()
897
+
838
898
  if options is not None:
839
- for key, value in options.items():
840
- if not isinstance(value, str):
841
- options[key] = json.dumps(value)
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)
842
907
 
843
908
  payload = {}
844
909
  if upload_id_used:
845
910
  payload["upload_id"] = upload_id
846
911
  else:
847
- payload["input"] = input
912
+ payload["input"] = input_data
848
913
 
849
914
  if name is not None:
850
915
  payload["name"] = name
851
916
  if description is not None:
852
917
  payload["description"] = description
853
- if options is not None:
854
- payload["options"] = options
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
855
923
  if configuration is not None:
856
- payload["configuration"] = configuration.to_dict()
924
+ configuration_dict = (
925
+ configuration.to_dict() if isinstance(configuration, RunConfiguration) else configuration
926
+ )
927
+ payload["configuration"] = configuration_dict
857
928
  if batch_experiment_id is not None:
858
929
  payload["batch_experiment_id"] = batch_experiment_id
859
930
  if external_result is not None:
860
- payload["result"] = external_result.to_dict()
931
+ external_dict = (
932
+ external_result.to_dict() if isinstance(external_result, ExternalRunResult) else external_result
933
+ )
934
+ payload["result"] = external_dict
861
935
 
862
936
  query_params = {
863
937
  "instance_id": instance_id if instance_id is not None else self.default_instance_id,
@@ -873,42 +947,83 @@ class Application:
873
947
 
874
948
  def new_run_with_result(
875
949
  self,
876
- input: Union[dict[str, Any], BaseModel] = None,
950
+ input: Union[Input, dict[str, Any], BaseModel, str] = None,
877
951
  instance_id: Optional[str] = None,
878
952
  name: Optional[str] = None,
879
953
  description: Optional[str] = None,
880
954
  upload_id: Optional[str] = None,
881
- run_options: Optional[dict[str, str]] = None,
955
+ run_options: Optional[Union[Options, dict[str, str]]] = None,
882
956
  polling_options: PollingOptions = _DEFAULT_POLLING_OPTIONS,
883
- configuration: Optional[RunConfiguration] = None,
957
+ configuration: Optional[Union[RunConfiguration, dict[str, any]]] = None,
884
958
  batch_experiment_id: Optional[str] = None,
885
- external_result: Optional[ExternalRunResult] = None,
959
+ external_result: Optional[Union[ExternalRunResult, dict[str, any]]] = None,
886
960
  ) -> RunResult:
887
961
  """
888
962
  Submit an input to start a new run of the application and poll for the
889
- result. This is a convenience method that combines the new_run and
890
- 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
891
965
  the run succeeded.
892
966
 
893
- Args:
894
- input: Input to use for the run.
895
- instance_id: ID of the instance to use for the run. If not
896
- provided, the default_instance_id will be used.
897
- name: Name of the run.
898
- description: Description of the run.
899
- upload_id: ID to use when running a large input.
900
- run_options: Options to use for the run.
901
- polling_options: Options to use when polling for the run result.
902
- configuration: Configuration to use for the run.
903
- batch_experimemt_id: ID of a batch experiment to associate the run
904
- with.
905
- external_result: External result to use for the run, if this is an
906
- external run
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.
907
1015
 
908
- Returns:
1016
+ Returns
1017
+ ----------
1018
+ RunResult
909
1019
  Result of the run.
910
1020
 
911
- 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]`.
912
1027
  requests.HTTPError: If the response status code is not 2xx.
913
1028
  TimeoutError: If the run does not succeed after the polling
914
1029
  strategy is exhausted based on time duration.
@@ -1652,7 +1767,7 @@ class Application:
1652
1767
  )
1653
1768
 
1654
1769
 
1655
- def poll(polling_options: PollingOptions, polling_func: callable) -> any:
1770
+ def poll(polling_options: PollingOptions, polling_func: Callable[[], tuple[any, bool]]) -> any:
1656
1771
  """
1657
1772
  Auxiliary function for polling.
1658
1773
 
@@ -1685,9 +1800,16 @@ def poll(polling_options: PollingOptions, polling_func: callable) -> any:
1685
1800
  time.sleep(polling_options.initial_delay)
1686
1801
 
1687
1802
  start_time = time.time()
1803
+ stopped = False
1688
1804
 
1689
1805
  # Begin the polling process.
1690
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
+
1691
1813
  # We check if we can stop polling.
1692
1814
  result, ok = polling_func()
1693
1815
  if polling_options.verbose:
@@ -1718,6 +1840,11 @@ def poll(polling_options: PollingOptions, polling_func: callable) -> any:
1718
1840
 
1719
1841
  time.sleep(sleep_duration)
1720
1842
 
1843
+ if stopped:
1844
+ log("polling | stop condition met, stopping polling")
1845
+
1846
+ return None
1847
+
1721
1848
  raise RuntimeError(
1722
1849
  f"polling did not succeed after {polling_options.max_tries} tries",
1723
1850
  )
nextmv/options.py CHANGED
@@ -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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nextmv
3
- Version: 0.21.0
3
+ Version: 0.22.0
4
4
  Summary: The all-purpose Python SDK for Nextmv
5
5
  Project-URL: Homepage, https://www.nextmv.io
6
6
  Project-URL: Documentation, https://www.nextmv.io/docs/python-sdks/nextmv/installation
@@ -1,16 +1,16 @@
1
- nextmv/__about__.py,sha256=cVvubA2yQRXFVwQqlSnbiwiGxtKCAPt8AmgS0WPjXVw,24
1
+ nextmv/__about__.py,sha256=m2IBVb3nBsm69PZPmYL-3F-v73dlOk3VgdBnwbuKFqo,24
2
2
  nextmv/__entrypoint__.py,sha256=o6xYGBBUgCY_htteW-qNJfp-S3Th8dzS4RreJcDCnzM,1333
3
3
  nextmv/__init__.py,sha256=rz9oP7JGyFQJdUtYX4j9xx4Gv4LMGfNvXoEID5qUqig,1354
4
4
  nextmv/base_model.py,sha256=mdaBe-epNK1cFgP4TxbOtn3So4pCi1vMTOrIBkCBp7A,1050
5
5
  nextmv/input.py,sha256=tYjiD9KM37Phqzm4faO4QLem8IXnU6HS160c1E-Yhc8,12732
6
6
  nextmv/logger.py,sha256=5qQ7E3Aaw3zzkIeiUuwYGzTcB7VhVqIzNZm5PHmdpUI,852
7
7
  nextmv/model.py,sha256=rwBdgmKSEp1DPv43zF0azj3QnbHO6O6wKs0PIGvVS40,9861
8
- nextmv/options.py,sha256=DZmjNxZyaTwnG2MEEKbIiBx3hI5F2DG70rY7DdeX44M,17336
8
+ nextmv/options.py,sha256=uLMGNt8MoBmtrcZSuinDZ7z8yq58bzHKohBRqC80r_s,18225
9
9
  nextmv/output.py,sha256=cxw7Z0dTj5u42w2MQLOz2gesj046tvYFSWmhSZtRSXU,20082
10
10
  nextmv/cloud/__init__.py,sha256=ojgA2vQRQnR8QO2tJme1TbGrjhr9yq9LIauxKJ5F40U,3305
11
11
  nextmv/cloud/acceptance_test.py,sha256=NtqGhj-UYibxGBbU2kfjr-lYcngojb_5VMvK2WZwibI,6620
12
12
  nextmv/cloud/account.py,sha256=mZUGzV-uMGBA5BC_FPtsiCMFuz5jxEZ3O1BbELZIm18,1841
13
- nextmv/cloud/application.py,sha256=cmgXz9J9pdugPVqTioqzXh_1iidY0Gcbr11e_JLat3E,55765
13
+ nextmv/cloud/application.py,sha256=XZvR0e2sx3HPLp6nhZEbwx7xytDSFRgDoyFDkIrGSWs,62362
14
14
  nextmv/cloud/batch_experiment.py,sha256=UxrMNAm2c7-RZ9TWRhZBtiVyEeUG1pjsLNhYkoT5Jog,2236
15
15
  nextmv/cloud/client.py,sha256=E4C8wOGOdgyMf-DCDt4YfI7ib8HwmUhAtKMdrSBJc2M,9004
16
16
  nextmv/cloud/input_set.py,sha256=ovkP17-jYs0yWrbqTM6Nl5ubWQabD_UrDqAHNo8aE2s,672
@@ -21,7 +21,7 @@ nextmv/cloud/run.py,sha256=4hEANGXysDiOI1SQtfs7t_IVk-bxCldIs2TOvCI7a3E,8689
21
21
  nextmv/cloud/secrets.py,sha256=kqlN4ceww_L4kVTrAU8BZykRzXINO3zhMT_BLYea6tk,1764
22
22
  nextmv/cloud/status.py,sha256=C-ax8cLw0jPeh7CPsJkCa0s4ImRyFI4NDJJxI0_1sr4,602
23
23
  nextmv/cloud/version.py,sha256=sjVRNRtohHA97j6IuyM33_DSSsXYkZPusYgpb6hlcrc,1244
24
- nextmv-0.21.0.dist-info/METADATA,sha256=GYySweVUa_akR4revy2w1dgfgBoPJBD7q4AL95EiIeE,14557
25
- nextmv-0.21.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
- nextmv-0.21.0.dist-info/licenses/LICENSE,sha256=ZIbK-sSWA-OZprjNbmJAglYRtl5_K4l9UwAV3PGJAPc,11349
27
- nextmv-0.21.0.dist-info/RECORD,,
24
+ nextmv-0.22.0.dist-info/METADATA,sha256=ekIfM1lCU_4J-WOBWbYjY54MRq74rFp6xHGUWtcIaC4,14557
25
+ nextmv-0.22.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
+ nextmv-0.22.0.dist-info/licenses/LICENSE,sha256=ZIbK-sSWA-OZprjNbmJAglYRtl5_K4l9UwAV3PGJAPc,11349
27
+ nextmv-0.22.0.dist-info/RECORD,,