nextmv 0.29.4.dev1__tar.gz → 0.29.5.dev1__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.
Files changed (71) hide show
  1. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/PKG-INFO +1 -1
  2. nextmv-0.29.5.dev1/nextmv/__about__.py +1 -0
  3. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/cloud/__init__.py +1 -0
  4. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/cloud/application.py +209 -100
  5. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/cloud/manifest.py +36 -2
  6. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/cloud/run.py +98 -48
  7. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/cloud/safe.py +30 -0
  8. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/model.py +0 -1
  9. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/options.py +14 -0
  10. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/cloud/app.yaml +5 -0
  11. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/cloud/test_manifest.py +40 -25
  12. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/test_entrypoint/test_entrypoint.py +2 -1
  13. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/test_model.py +2 -1
  14. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/test_options.py +2 -1
  15. nextmv-0.29.4.dev1/nextmv/__about__.py +0 -1
  16. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/.gitignore +0 -0
  17. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/LICENSE +0 -0
  18. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/README.md +0 -0
  19. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/__entrypoint__.py +0 -0
  20. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/__init__.py +0 -0
  21. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/_serialization.py +0 -0
  22. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/base_model.py +0 -0
  23. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/cloud/acceptance_test.py +0 -0
  24. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/cloud/account.py +0 -0
  25. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/cloud/batch_experiment.py +0 -0
  26. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/cloud/client.py +0 -0
  27. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/cloud/input_set.py +0 -0
  28. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/cloud/instance.py +0 -0
  29. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/cloud/package.py +0 -0
  30. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/cloud/scenario.py +0 -0
  31. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/cloud/secrets.py +0 -0
  32. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/cloud/status.py +0 -0
  33. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/cloud/version.py +0 -0
  34. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/default_app/README.md +0 -0
  35. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/default_app/app.yaml +0 -0
  36. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/default_app/requirements.txt +0 -0
  37. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/default_app/src/__init__.py +0 -0
  38. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/default_app/src/main.py +0 -0
  39. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/default_app/src/visuals.py +0 -0
  40. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/deprecated.py +0 -0
  41. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/input.py +0 -0
  42. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/logger.py +0 -0
  43. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/nextmv/output.py +0 -0
  44. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/pyproject.toml +0 -0
  45. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/__init__.py +0 -0
  46. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/cloud/__init__.py +0 -0
  47. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/cloud/test_application.py +0 -0
  48. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/cloud/test_client.py +0 -0
  49. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/cloud/test_package.py +0 -0
  50. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/cloud/test_run.py +0 -0
  51. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/cloud/test_safe_name_id.py +0 -0
  52. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/cloud/test_scenario.py +0 -0
  53. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/scripts/__init__.py +0 -0
  54. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/scripts/options1.py +0 -0
  55. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/scripts/options2.py +0 -0
  56. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/scripts/options3.py +0 -0
  57. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/scripts/options4.py +0 -0
  58. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/scripts/options5.py +0 -0
  59. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/scripts/options6.py +0 -0
  60. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/scripts/options7.py +0 -0
  61. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/scripts/options_deprecated.py +0 -0
  62. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/test_base_model.py +0 -0
  63. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/test_entrypoint/__init__.py +0 -0
  64. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/test_input.py +0 -0
  65. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/test_inputs/test_data.csv +0 -0
  66. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/test_inputs/test_data.json +0 -0
  67. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/test_inputs/test_data.txt +0 -0
  68. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/test_logger.py +0 -0
  69. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/test_output.py +1 -1
  70. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/test_serialization.py +0 -0
  71. {nextmv-0.29.4.dev1 → nextmv-0.29.5.dev1}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nextmv
3
- Version: 0.29.4.dev1
3
+ Version: 0.29.5.dev1
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://nextmv-py.docs.nextmv.io/en/latest/nextmv/
@@ -0,0 +1 @@
1
+ __version__ = "v0.29.5.dev.1"
@@ -46,6 +46,7 @@ from .run import ErrorLog as ErrorLog
46
46
  from .run import ExternalRunResult as ExternalRunResult
47
47
  from .run import Format as Format
48
48
  from .run import FormatInput as FormatInput
49
+ from .run import FormatOutput as FormatOutput
49
50
  from .run import Metadata as Metadata
50
51
  from .run import RunConfiguration as RunConfiguration
51
52
  from .run import RunInformation as RunInformation
@@ -56,13 +56,14 @@ from nextmv.cloud.run import (
56
56
  ExternalRunResult,
57
57
  Format,
58
58
  FormatInput,
59
+ FormatOutput,
59
60
  RunConfiguration,
60
61
  RunInformation,
61
62
  RunLog,
62
63
  RunResult,
63
64
  TrackedRun,
64
65
  )
65
- from nextmv.cloud.safe import _name_and_id
66
+ from nextmv.cloud.safe import _name_and_id, _safe_id
66
67
  from nextmv.cloud.scenario import Scenario, ScenarioInputType, _option_sets, _scenarios_by_id
67
68
  from nextmv.cloud.secrets import Secret, SecretsCollection, SecretsCollectionSummary
68
69
  from nextmv.cloud.status import StatusV2
@@ -71,7 +72,7 @@ from nextmv.input import Input, InputFormat
71
72
  from nextmv.logger import log
72
73
  from nextmv.model import Model, ModelConfiguration
73
74
  from nextmv.options import Options
74
- from nextmv.output import Output
75
+ from nextmv.output import Output, OutputFormat
75
76
 
76
77
  # Maximum size of the run input/output in bytes. This constant defines the
77
78
  # maximum allowed size for run inputs and outputs. When the size exceeds this
@@ -1127,16 +1128,26 @@ class Application:
1127
1128
  f"batch experiment {id} does not exist, input_set_id must be defined to create a new one"
1128
1129
  ) from e
1129
1130
  else:
1130
- runs = [
1131
- BatchExperimentRun(
1132
- instance_id=candidate_instance_id,
1133
- input_set_id=input_set_id,
1134
- ),
1135
- BatchExperimentRun(
1136
- instance_id=baseline_instance_id,
1137
- input_set_id=input_set_id,
1138
- ),
1139
- ]
1131
+ # Get all input IDs from the input set.
1132
+ input_set = self.input_set(input_set_id=input_set_id)
1133
+ if len(input_set.input_ids) == 0:
1134
+ raise ValueError(f"input set {input_set_id} does not contain any inputs")
1135
+ runs = []
1136
+ for input_id in input_set.input_ids:
1137
+ runs.append(
1138
+ BatchExperimentRun(
1139
+ instance_id=candidate_instance_id,
1140
+ input_set_id=input_set_id,
1141
+ input_id=input_id,
1142
+ )
1143
+ )
1144
+ runs.append(
1145
+ BatchExperimentRun(
1146
+ instance_id=baseline_instance_id,
1147
+ input_set_id=input_set_id,
1148
+ input_id=input_id,
1149
+ )
1150
+ )
1140
1151
  batch_experiment_id = self.new_batch_experiment(
1141
1152
  name=name,
1142
1153
  description=description,
@@ -1588,7 +1599,10 @@ class Application:
1588
1599
  if format is not None:
1589
1600
  payload["format"] = format.to_dict() if isinstance(format, Format) else format
1590
1601
  else:
1591
- payload["format"] = Format(format_input=FormatInput(input_type=InputFormat.JSON)).to_dict()
1602
+ payload["format"] = Format(
1603
+ format_input=FormatInput(input_type=InputFormat.JSON),
1604
+ format_output=FormatOutput(output_type=OutputFormat.JSON),
1605
+ ).to_dict()
1592
1606
 
1593
1607
  response = self.client.request(
1594
1608
  method="POST",
@@ -1610,7 +1624,7 @@ class Application:
1610
1624
  batch_experiment_id: Optional[str] = None,
1611
1625
  external_result: Optional[Union[ExternalRunResult, dict[str, Any]]] = None,
1612
1626
  json_configurations: Optional[dict[str, Any]] = None,
1613
- dir_path: Optional[str] = None,
1627
+ input_dir_path: Optional[str] = None,
1614
1628
  ) -> str:
1615
1629
  """
1616
1630
  Submit an input to start a new run of the application. Returns the
@@ -1627,14 +1641,14 @@ class Application:
1627
1641
  input data is extracted from the `.data` property.
1628
1642
 
1629
1643
  If you want to work with `nextmv.InputFormat.CSV_ARCHIVE` or
1630
- `nextmv.InputFormat.MULTI_FILE`, you should use the `dir_path`
1644
+ `nextmv.InputFormat.MULTI_FILE`, you should use the `input_dir_path`
1631
1645
  argument instead. This argument takes precedence over the `input`.
1632
- If `dir_path` is specified, this function looks for files in that
1646
+ If `input_dir_path` is specified, this function looks for files in that
1633
1647
  directory and tars them, to later be uploaded using the
1634
- `upload_large_input` method. If both the `dir_path` and `input`
1648
+ `upload_large_input` method. If both the `input_dir_path` and `input`
1635
1649
  arguments are provided, the `input` is ignored.
1636
1650
 
1637
- When `dir_path` is specified, the `configuration` argument must
1651
+ When `input_dir_path` is specified, the `configuration` argument must
1638
1652
  also be provided. More specifically, the
1639
1653
  `RunConfiguration.format.format_input.input_type` parameter
1640
1654
  dictates what kind of input is being submitted to the Nextmv Cloud.
@@ -1686,12 +1700,12 @@ class Application:
1686
1700
  json_configurations: Optional[dict[str, Any]]
1687
1701
  Optional configurations for JSON serialization. This is used to
1688
1702
  customize the serialization before data is sent.
1689
- dir_path: Optional[str]
1703
+ input_dir_path: Optional[str]
1690
1704
  Path to a directory containing input files. If specified, the
1691
1705
  function will package the files in the directory into a tar file
1692
1706
  and upload it as a large input. This is useful for input formats
1693
1707
  like `nextmv.InputFormat.CSV_ARCHIVE` or `nextmv.InputFormat.MULTI_FILE`.
1694
- If both `input` and `dir_path` are specified, the `input` is
1708
+ If both `input` and `input_dir_path` are specified, the `input` is
1695
1709
  ignored, and the files in the directory are used instead.
1696
1710
 
1697
1711
  Returns
@@ -1708,17 +1722,17 @@ class Application:
1708
1722
  not `JSON`. If the final `options` are not of type `dict[str,str]`.
1709
1723
  """
1710
1724
 
1711
- self.__validate_dir_path_and_configuration(dir_path, configuration)
1725
+ self.__validate_dir_path_and_configuration(input_dir_path, configuration)
1712
1726
 
1713
1727
  tar_file = ""
1714
- if dir_path is not None and dir_path != "":
1715
- if not os.path.exists(dir_path):
1716
- raise ValueError(f"Directory {dir_path} does not exist.")
1728
+ if input_dir_path is not None and input_dir_path != "":
1729
+ if not os.path.exists(input_dir_path):
1730
+ raise ValueError(f"Directory {input_dir_path} does not exist.")
1717
1731
 
1718
- if not os.path.isdir(dir_path):
1719
- raise ValueError(f"Path {dir_path} is not a directory.")
1732
+ if not os.path.isdir(input_dir_path):
1733
+ raise ValueError(f"Path {input_dir_path} is not a directory.")
1720
1734
 
1721
- tar_file = self.__package_inputs(dir_path)
1735
+ tar_file = self.__package_inputs(input_dir_path)
1722
1736
 
1723
1737
  input_data = None
1724
1738
  if isinstance(input, BaseModel):
@@ -1775,7 +1789,7 @@ class Application:
1775
1789
  )
1776
1790
  else:
1777
1791
  configuration = RunConfiguration()
1778
- configuration.resolve(input=input, dir_path=dir_path)
1792
+ configuration.resolve(input=input, dir_path=input_dir_path)
1779
1793
  configuration_dict = configuration.to_dict()
1780
1794
 
1781
1795
  payload["configuration"] = configuration_dict
@@ -1814,7 +1828,8 @@ class Application:
1814
1828
  batch_experiment_id: Optional[str] = None,
1815
1829
  external_result: Optional[Union[ExternalRunResult, dict[str, Any]]] = None,
1816
1830
  json_configurations: Optional[dict[str, Any]] = None,
1817
- dir_path: Optional[str] = None,
1831
+ input_dir_path: Optional[str] = None,
1832
+ output_dir_path: Optional[str] = ".",
1818
1833
  ) -> RunResult:
1819
1834
  """
1820
1835
  Submit an input to start a new run of the application and poll for the
@@ -1833,14 +1848,14 @@ class Application:
1833
1848
  input data is extracted from the `.data` property.
1834
1849
 
1835
1850
  If you want to work with `nextmv.InputFormat.CSV_ARCHIVE` or
1836
- `nextmv.InputFormat.MULTI_FILE`, you should use the `dir_path`
1851
+ `nextmv.InputFormat.MULTI_FILE`, you should use the `input_dir_path`
1837
1852
  argument instead. This argument takes precedence over the `input`.
1838
- If `dir_path` is specified, this function looks for files in that
1853
+ If `input_dir_path` is specified, this function looks for files in that
1839
1854
  directory and tars them, to later be uploaded using the
1840
- `upload_large_input` method. If both the `dir_path` and `input`
1855
+ `upload_large_input` method. If both the `input_dir_path` and `input`
1841
1856
  arguments are provided, the `input` is ignored.
1842
1857
 
1843
- When `dir_path` is specified, the `configuration` argument must
1858
+ When `input_dir_path` is specified, the `configuration` argument must
1844
1859
  also be provided. More specifically, the
1845
1860
  `RunConfiguration.format.format_input.input_type` parameter
1846
1861
  dictates what kind of input is being submitted to the Nextmv Cloud.
@@ -1897,13 +1912,17 @@ class Application:
1897
1912
  json_configurations: Optional[dict[str, Any]]
1898
1913
  Optional configurations for JSON serialization. This is used to
1899
1914
  customize the serialization before data is sent.
1900
- dir_path: Optional[str]
1915
+ input_dir_path: Optional[str]
1901
1916
  Path to a directory containing input files. If specified, the
1902
1917
  function will package the files in the directory into a tar file
1903
1918
  and upload it as a large input. This is useful for input formats
1904
1919
  like `nextmv.InputFormat.CSV_ARCHIVE` or `nextmv.InputFormat.MULTI_FILE`.
1905
- If both `input` and `dir_path` are specified, the `input` is
1920
+ If both `input` and `input_dir_path` are specified, the `input` is
1906
1921
  ignored, and the files in the directory are used instead.
1922
+ output_dir_path : Optional[str], default="."
1923
+ Path to a directory where non-JSON output files will be saved. This is
1924
+ required if the output is non-JSON. If the directory does not exist, it
1925
+ will be created. Uses the current directory by default.
1907
1926
 
1908
1927
  Returns
1909
1928
  ----------
@@ -1936,12 +1955,13 @@ class Application:
1936
1955
  batch_experiment_id=batch_experiment_id,
1937
1956
  external_result=external_result,
1938
1957
  json_configurations=json_configurations,
1939
- dir_path=dir_path,
1958
+ input_dir_path=input_dir_path,
1940
1959
  )
1941
1960
 
1942
1961
  return self.run_result_with_polling(
1943
1962
  run_id=run_id,
1944
1963
  polling_options=polling_options,
1964
+ output_dir_path=output_dir_path,
1945
1965
  )
1946
1966
 
1947
1967
  def new_scenario_test(
@@ -2215,10 +2235,13 @@ class Application:
2215
2235
  if exist_ok and self.version_exists(version_id=id):
2216
2236
  return self.version(version_id=id)
2217
2237
 
2218
- payload = {}
2238
+ if id is None:
2239
+ id = _safe_id(prefix="version")
2240
+
2241
+ payload = {
2242
+ "id": id,
2243
+ }
2219
2244
 
2220
- if id is not None:
2221
- payload["id"] = id
2222
2245
  if name is not None:
2223
2246
  payload["name"] = name
2224
2247
  if description is not None:
@@ -2501,7 +2524,7 @@ class Application:
2501
2524
  )
2502
2525
  return RunLog.from_dict(response.json())
2503
2526
 
2504
- def run_result(self, run_id: str) -> RunResult:
2527
+ def run_result(self, run_id: str, output_dir_path: Optional[str] = ".") -> RunResult:
2505
2528
  """
2506
2529
  Get the result of a run.
2507
2530
 
@@ -2511,6 +2534,10 @@ class Application:
2511
2534
  ----------
2512
2535
  run_id : str
2513
2536
  ID of the run to get results for.
2537
+ output_dir_path : Optional[str], default="."
2538
+ Path to a directory where non-JSON output files will be saved. This is
2539
+ required if the output is non-JSON. If the directory does not exist, it
2540
+ will be created. Uses the current directory by default.
2514
2541
 
2515
2542
  Returns
2516
2543
  -------
@@ -2531,12 +2558,17 @@ class Application:
2531
2558
 
2532
2559
  run_information = self.run_metadata(run_id=run_id)
2533
2560
 
2534
- return self.__run_result(run_id=run_id, run_information=run_information)
2561
+ return self.__run_result(
2562
+ run_id=run_id,
2563
+ run_information=run_information,
2564
+ output_dir_path=output_dir_path,
2565
+ )
2535
2566
 
2536
2567
  def run_result_with_polling(
2537
2568
  self,
2538
2569
  run_id: str,
2539
2570
  polling_options: PollingOptions = _DEFAULT_POLLING_OPTIONS,
2571
+ output_dir_path: Optional[str] = ".",
2540
2572
  ) -> RunResult:
2541
2573
  """
2542
2574
  Get the result of a run with polling.
@@ -2551,6 +2583,10 @@ class Application:
2551
2583
  ID of the run to retrieve the result for.
2552
2584
  polling_options : PollingOptions, default=_DEFAULT_POLLING_OPTIONS
2553
2585
  Options to use when polling for the run result.
2586
+ output_dir_path : Optional[str], default="."
2587
+ Path to a directory where non-JSON output files will be saved. This is
2588
+ required if the output is non-JSON. If the directory does not exist, it
2589
+ will be created. Uses the current directory by default.
2554
2590
 
2555
2591
  Returns
2556
2592
  -------
@@ -2592,7 +2628,11 @@ class Application:
2592
2628
 
2593
2629
  run_information = poll(polling_options=polling_options, polling_func=polling_func)
2594
2630
 
2595
- return self.__run_result(run_id=run_id, run_information=run_information)
2631
+ return self.__run_result(
2632
+ run_id=run_id,
2633
+ run_information=run_information,
2634
+ output_dir_path=output_dir_path,
2635
+ )
2596
2636
 
2597
2637
  def scenario_test(self, scenario_test_id: str) -> BatchExperiment:
2598
2638
  """
@@ -2707,6 +2747,7 @@ class Application:
2707
2747
  tracked_run: TrackedRun,
2708
2748
  polling_options: PollingOptions = _DEFAULT_POLLING_OPTIONS,
2709
2749
  instance_id: Optional[str] = None,
2750
+ output_dir_path: Optional[str] = ".",
2710
2751
  ) -> RunResult:
2711
2752
  """
2712
2753
  Track an external run and poll for the result. This is a convenience
@@ -2723,6 +2764,10 @@ class Application:
2723
2764
  instance_id: Optional[str]
2724
2765
  Optional instance ID if you want to associate your tracked run with
2725
2766
  an instance.
2767
+ output_dir_path : Optional[str], default="."
2768
+ Path to a directory where non-JSON output files will be saved. This is
2769
+ required if the output is non-JSON. If the directory does not exist, it
2770
+ will be created. Uses the current directory by default.
2726
2771
 
2727
2772
  Returns
2728
2773
  -------
@@ -2747,12 +2792,13 @@ class Application:
2747
2792
  return self.run_result_with_polling(
2748
2793
  run_id=run_id,
2749
2794
  polling_options=polling_options,
2795
+ output_dir_path=output_dir_path,
2750
2796
  )
2751
2797
 
2752
2798
  def update_instance(
2753
2799
  self,
2754
2800
  id: str,
2755
- name: str,
2801
+ name: Optional[str] = None,
2756
2802
  version_id: Optional[str] = None,
2757
2803
  description: Optional[str] = None,
2758
2804
  configuration: Optional[InstanceConfiguration] = None,
@@ -2764,14 +2810,14 @@ class Application:
2764
2810
  ----------
2765
2811
  id : str
2766
2812
  ID of the instance to update.
2767
- name : str
2768
- Name of the instance.
2813
+ name : Optional[str], default=None
2814
+ Optional name of the instance.
2769
2815
  version_id : Optional[str], default=None
2770
- ID of the version to associate the instance with.
2816
+ Optional ID of the version to associate the instance with.
2771
2817
  description : Optional[str], default=None
2772
- Description of the instance.
2818
+ Optional description of the instance.
2773
2819
  configuration : Optional[InstanceConfiguration], default=None
2774
- Configuration to use for the instance.
2820
+ Optional configuration to use for the instance.
2775
2821
 
2776
2822
  Returns
2777
2823
  -------
@@ -2784,12 +2830,21 @@ class Application:
2784
2830
  If the response status code is not 2xx.
2785
2831
  """
2786
2832
 
2787
- payload = {}
2833
+ # Get the instance as it currently exsits.
2834
+ instance = self.instance(id)
2835
+ instance_dict = instance.to_dict()
2836
+
2837
+ payload = {
2838
+ "name": instance_dict["name"],
2839
+ "version_id": instance_dict["version_id"],
2840
+ "description": instance_dict["description"],
2841
+ "configuration": instance_dict["configuration"],
2842
+ }
2788
2843
 
2789
- if version_id is not None:
2790
- payload["version_id"] = version_id
2791
2844
  if name is not None:
2792
2845
  payload["name"] = name
2846
+ if version_id is not None:
2847
+ payload["version_id"] = version_id
2793
2848
  if description is not None:
2794
2849
  payload["description"] = description
2795
2850
  if configuration is not None:
@@ -2806,8 +2861,8 @@ class Application:
2806
2861
  def update_batch_experiment(
2807
2862
  self,
2808
2863
  batch_experiment_id: str,
2809
- name: str,
2810
- description: str,
2864
+ name: Optional[str] = None,
2865
+ description: Optional[str] = None,
2811
2866
  ) -> BatchExperimentInformation:
2812
2867
  """
2813
2868
  Update a batch experiment.
@@ -2816,10 +2871,10 @@ class Application:
2816
2871
  ----------
2817
2872
  batch_experiment_id : str
2818
2873
  ID of the batch experiment to update.
2819
- name : str
2820
- Name of the batch experiment.
2821
- description : str
2822
- Description of the batch experiment.
2874
+ name : Optional[str], default=None
2875
+ Optional name of the batch experiment.
2876
+ description : Optional[str], default=None
2877
+ Optional description of the batch experiment.
2823
2878
 
2824
2879
  Returns
2825
2880
  -------
@@ -2832,10 +2887,13 @@ class Application:
2832
2887
  If the response status code is not 2xx.
2833
2888
  """
2834
2889
 
2835
- payload = {
2836
- "name": name,
2837
- "description": description,
2838
- }
2890
+ payload = {}
2891
+
2892
+ if name is not None:
2893
+ payload["name"] = name
2894
+ if description is not None:
2895
+ payload["description"] = description
2896
+
2839
2897
  response = self.client.request(
2840
2898
  method="PATCH",
2841
2899
  endpoint=f"{self.experiments_endpoint}/batch/{batch_experiment_id}",
@@ -2847,9 +2905,9 @@ class Application:
2847
2905
  def update_managed_input(
2848
2906
  self,
2849
2907
  managed_input_id: str,
2850
- name: str,
2851
- description: str,
2852
- ) -> None:
2908
+ name: Optional[str] = None,
2909
+ description: Optional[str] = None,
2910
+ ) -> ManagedInput:
2853
2911
  """
2854
2912
  Update a managed input.
2855
2913
 
@@ -2857,15 +2915,15 @@ class Application:
2857
2915
  ----------
2858
2916
  managed_input_id : str
2859
2917
  ID of the managed input to update.
2860
- name : str
2861
- Name of the managed input.
2862
- description : str
2863
- Description of the managed input.
2918
+ name : Optional[str], default=None
2919
+ Optional new name for the managed input.
2920
+ description : Optional[str], default=None
2921
+ Optional new description for the managed input.
2864
2922
 
2865
2923
  Returns
2866
2924
  -------
2867
- None
2868
- No return value.
2925
+ ManagedInput
2926
+ The updated managed input.
2869
2927
 
2870
2928
  Raises
2871
2929
  ------
@@ -2873,21 +2931,32 @@ class Application:
2873
2931
  If the response status code is not 2xx.
2874
2932
  """
2875
2933
 
2934
+ managed_input = self.managed_input(managed_input_id)
2935
+ managed_input_dict = managed_input.to_dict()
2936
+
2876
2937
  payload = {
2877
- "name": name,
2878
- "description": description,
2938
+ "name": managed_input_dict["name"],
2939
+ "description": managed_input_dict["description"],
2879
2940
  }
2880
- _ = self.client.request(
2941
+
2942
+ if name is not None:
2943
+ payload["name"] = name
2944
+ if description is not None:
2945
+ payload["description"] = description
2946
+
2947
+ response = self.client.request(
2881
2948
  method="PUT",
2882
2949
  endpoint=f"{self.endpoint}/inputs/{managed_input_id}",
2883
2950
  payload=payload,
2884
2951
  )
2885
2952
 
2953
+ return ManagedInput.from_dict(response.json())
2954
+
2886
2955
  def update_scenario_test(
2887
2956
  self,
2888
2957
  scenario_test_id: str,
2889
- name: str,
2890
- description: str,
2958
+ name: Optional[str] = None,
2959
+ description: Optional[str] = None,
2891
2960
  ) -> BatchExperimentInformation:
2892
2961
  """
2893
2962
  Update a scenario test.
@@ -2900,10 +2969,10 @@ class Application:
2900
2969
  ----------
2901
2970
  scenario_test_id : str
2902
2971
  ID of the scenario test to update.
2903
- name : str
2904
- New name for the scenario test.
2905
- description : str
2906
- New description for the scenario test.
2972
+ name : Optional[str], default=None
2973
+ Optional new name for the scenario test.
2974
+ description : Optional[str], default=None
2975
+ Optional new description for the scenario test.
2907
2976
 
2908
2977
  Returns
2909
2978
  -------
@@ -2935,9 +3004,9 @@ class Application:
2935
3004
  def update_secrets_collection(
2936
3005
  self,
2937
3006
  secrets_collection_id: str,
2938
- name: str,
2939
- description: str,
2940
- secrets: list[Secret],
3007
+ name: Optional[str] = None,
3008
+ description: Optional[str] = None,
3009
+ secrets: Optional[list[Secret]] = None,
2941
3010
  ) -> SecretsCollectionSummary:
2942
3011
  """
2943
3012
  Update a secrets collection.
@@ -2950,13 +3019,13 @@ class Application:
2950
3019
  ----------
2951
3020
  secrets_collection_id : str
2952
3021
  ID of the secrets collection to update.
2953
- name : str
2954
- New name for the secrets collection.
2955
- description : str
2956
- New description for the secrets collection.
2957
- secrets : list[Secret]
2958
- List of secrets to update. Each secret should be an instance of the
2959
- Secret class containing a key and value.
3022
+ name : Optional[str], default=None
3023
+ Optional new name for the secrets collection.
3024
+ description : Optional[str], default=None
3025
+ Optional new description for the secrets collection.
3026
+ secrets : Optional[list[Secret]], default=None
3027
+ Optional list of secrets to update. Each secret should be an
3028
+ instance of the Secret class containing a key and value.
2960
3029
 
2961
3030
  Returns
2962
3031
  -------
@@ -2988,14 +3057,22 @@ class Application:
2988
3057
  'api-secrets'
2989
3058
  """
2990
3059
 
2991
- if len(secrets) == 0:
2992
- raise ValueError("secrets must be provided")
3060
+ collection = self.secrets_collection(secrets_collection_id)
3061
+ collection_dict = collection.to_dict()
2993
3062
 
2994
3063
  payload = {
2995
- "name": name,
2996
- "description": description,
2997
- "secrets": [secret.to_dict() for secret in secrets],
3064
+ "name": collection_dict["name"],
3065
+ "description": collection_dict["description"],
3066
+ "secrets": collection_dict["secrets"],
2998
3067
  }
3068
+
3069
+ if name is not None:
3070
+ payload["name"] = name
3071
+ if description is not None:
3072
+ payload["description"] = description
3073
+ if secrets is not None and len(secrets) > 0:
3074
+ payload["secrets"] = [secret.to_dict() for secret in secrets]
3075
+
2999
3076
  response = self.client.request(
3000
3077
  method="PUT",
3001
3078
  endpoint=f"{self.endpoint}/secrets/{secrets_collection_id}",
@@ -3229,6 +3306,7 @@ class Application:
3229
3306
  self,
3230
3307
  run_id: str,
3231
3308
  run_information: RunInformation,
3309
+ output_dir_path: Optional[str] = ".",
3232
3310
  ) -> RunResult:
3233
3311
  """
3234
3312
  Get the result of a run.
@@ -3245,6 +3323,10 @@ class Application:
3245
3323
  ID of the run to retrieve the result for.
3246
3324
  run_information : RunInformation
3247
3325
  Information about the run, including metadata such as output size.
3326
+ output_dir_path : Optional[str], default="."
3327
+ Path to a directory where non-JSON output files will be saved. This is
3328
+ required if the output is non-JSON. If the directory does not exist, it
3329
+ will be created. Uses the current directory by default.
3248
3330
 
3249
3331
  Returns
3250
3332
  -------
@@ -3265,10 +3347,13 @@ class Application:
3265
3347
  a download URL and fetch the output data separately.
3266
3348
  """
3267
3349
  query_params = None
3268
- large_output = False
3269
- if run_information.metadata.output_size > _MAX_RUN_SIZE:
3350
+ use_presigned_url = False
3351
+ if (
3352
+ run_information.metadata.format.format_output.output_type != OutputFormat.JSON
3353
+ or run_information.metadata.output_size > _MAX_RUN_SIZE
3354
+ ):
3270
3355
  query_params = {"format": "url"}
3271
- large_output = True
3356
+ use_presigned_url = True
3272
3357
 
3273
3358
  response = self.client.request(
3274
3359
  method="GET",
@@ -3278,7 +3363,7 @@ class Application:
3278
3363
  result = RunResult.from_dict(response.json())
3279
3364
  result.console_url = self.__console_url(result.id)
3280
3365
 
3281
- if not large_output:
3366
+ if not use_presigned_url or result.metadata.status_v2 != StatusV2.succeeded:
3282
3367
  return result
3283
3368
 
3284
3369
  download_url = DownloadURL.from_dict(response.json()["output"])
@@ -3287,7 +3372,24 @@ class Application:
3287
3372
  endpoint=download_url.url,
3288
3373
  headers={"Content-Type": "application/json"},
3289
3374
  )
3290
- result.output = download_response.json()
3375
+
3376
+ # See whether we can attach the output directly or need to save to the given
3377
+ # directory
3378
+ if run_information.metadata.format.format_output.output_type != OutputFormat.JSON:
3379
+ if not output_dir_path or output_dir_path == "":
3380
+ raise ValueError(
3381
+ "If the output format is not JSON, an output_dir_path must be provided.",
3382
+ )
3383
+ if not os.path.exists(output_dir_path):
3384
+ os.makedirs(output_dir_path, exist_ok=True)
3385
+ # Save .tar.gz file to a temp directory and extract contents to output_dir_path
3386
+ with tempfile.TemporaryDirectory() as tmpdirname:
3387
+ temp_tar_path = os.path.join(tmpdirname, f"{run_id}.tar.gz")
3388
+ with open(temp_tar_path, "wb") as f:
3389
+ f.write(download_response.content)
3390
+ shutil.unpack_archive(temp_tar_path, output_dir_path)
3391
+ else:
3392
+ result.output = download_response.json()
3291
3393
 
3292
3394
  return result
3293
3395
 
@@ -3325,7 +3427,14 @@ class Application:
3325
3427
  }
3326
3428
 
3327
3429
  if manifest.configuration is not None and manifest.configuration.options is not None:
3328
- activation_request["requirements"]["options"] = manifest.configuration.options.to_dict()
3430
+ options = manifest.configuration.options.to_dict()
3431
+ if "format" in options and isinstance(options["format"], list):
3432
+ # the endpoint expects a dictionary with a template key having a list of strings
3433
+ # the app.yaml however defines format as a list of strings, so we need to convert it here
3434
+ options["format"] = {
3435
+ "template": options["format"],
3436
+ }
3437
+ activation_request["requirements"]["options"] = options
3329
3438
 
3330
3439
  response = self.client.request(
3331
3440
  method="PUT",