nextmv 0.29.4.dev1__py3-none-any.whl → 0.29.5.dev0__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.29.4-dev.1"
1
+ __version__ = "v0.29.5.dev.0"
nextmv/cloud/__init__.py CHANGED
@@ -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
@@ -1588,7 +1589,10 @@ class Application:
1588
1589
  if format is not None:
1589
1590
  payload["format"] = format.to_dict() if isinstance(format, Format) else format
1590
1591
  else:
1591
- payload["format"] = Format(format_input=FormatInput(input_type=InputFormat.JSON)).to_dict()
1592
+ payload["format"] = Format(
1593
+ format_input=FormatInput(input_type=InputFormat.JSON),
1594
+ format_output=FormatOutput(output_type=OutputFormat.JSON),
1595
+ ).to_dict()
1592
1596
 
1593
1597
  response = self.client.request(
1594
1598
  method="POST",
@@ -1610,7 +1614,7 @@ class Application:
1610
1614
  batch_experiment_id: Optional[str] = None,
1611
1615
  external_result: Optional[Union[ExternalRunResult, dict[str, Any]]] = None,
1612
1616
  json_configurations: Optional[dict[str, Any]] = None,
1613
- dir_path: Optional[str] = None,
1617
+ input_dir_path: Optional[str] = None,
1614
1618
  ) -> str:
1615
1619
  """
1616
1620
  Submit an input to start a new run of the application. Returns the
@@ -1627,14 +1631,14 @@ class Application:
1627
1631
  input data is extracted from the `.data` property.
1628
1632
 
1629
1633
  If you want to work with `nextmv.InputFormat.CSV_ARCHIVE` or
1630
- `nextmv.InputFormat.MULTI_FILE`, you should use the `dir_path`
1634
+ `nextmv.InputFormat.MULTI_FILE`, you should use the `input_dir_path`
1631
1635
  argument instead. This argument takes precedence over the `input`.
1632
- If `dir_path` is specified, this function looks for files in that
1636
+ If `input_dir_path` is specified, this function looks for files in that
1633
1637
  directory and tars them, to later be uploaded using the
1634
- `upload_large_input` method. If both the `dir_path` and `input`
1638
+ `upload_large_input` method. If both the `input_dir_path` and `input`
1635
1639
  arguments are provided, the `input` is ignored.
1636
1640
 
1637
- When `dir_path` is specified, the `configuration` argument must
1641
+ When `input_dir_path` is specified, the `configuration` argument must
1638
1642
  also be provided. More specifically, the
1639
1643
  `RunConfiguration.format.format_input.input_type` parameter
1640
1644
  dictates what kind of input is being submitted to the Nextmv Cloud.
@@ -1686,12 +1690,12 @@ class Application:
1686
1690
  json_configurations: Optional[dict[str, Any]]
1687
1691
  Optional configurations for JSON serialization. This is used to
1688
1692
  customize the serialization before data is sent.
1689
- dir_path: Optional[str]
1693
+ input_dir_path: Optional[str]
1690
1694
  Path to a directory containing input files. If specified, the
1691
1695
  function will package the files in the directory into a tar file
1692
1696
  and upload it as a large input. This is useful for input formats
1693
1697
  like `nextmv.InputFormat.CSV_ARCHIVE` or `nextmv.InputFormat.MULTI_FILE`.
1694
- If both `input` and `dir_path` are specified, the `input` is
1698
+ If both `input` and `input_dir_path` are specified, the `input` is
1695
1699
  ignored, and the files in the directory are used instead.
1696
1700
 
1697
1701
  Returns
@@ -1708,17 +1712,17 @@ class Application:
1708
1712
  not `JSON`. If the final `options` are not of type `dict[str,str]`.
1709
1713
  """
1710
1714
 
1711
- self.__validate_dir_path_and_configuration(dir_path, configuration)
1715
+ self.__validate_dir_path_and_configuration(input_dir_path, configuration)
1712
1716
 
1713
1717
  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.")
1718
+ if input_dir_path is not None and input_dir_path != "":
1719
+ if not os.path.exists(input_dir_path):
1720
+ raise ValueError(f"Directory {input_dir_path} does not exist.")
1717
1721
 
1718
- if not os.path.isdir(dir_path):
1719
- raise ValueError(f"Path {dir_path} is not a directory.")
1722
+ if not os.path.isdir(input_dir_path):
1723
+ raise ValueError(f"Path {input_dir_path} is not a directory.")
1720
1724
 
1721
- tar_file = self.__package_inputs(dir_path)
1725
+ tar_file = self.__package_inputs(input_dir_path)
1722
1726
 
1723
1727
  input_data = None
1724
1728
  if isinstance(input, BaseModel):
@@ -1775,7 +1779,7 @@ class Application:
1775
1779
  )
1776
1780
  else:
1777
1781
  configuration = RunConfiguration()
1778
- configuration.resolve(input=input, dir_path=dir_path)
1782
+ configuration.resolve(input=input, dir_path=input_dir_path)
1779
1783
  configuration_dict = configuration.to_dict()
1780
1784
 
1781
1785
  payload["configuration"] = configuration_dict
@@ -1814,7 +1818,8 @@ class Application:
1814
1818
  batch_experiment_id: Optional[str] = None,
1815
1819
  external_result: Optional[Union[ExternalRunResult, dict[str, Any]]] = None,
1816
1820
  json_configurations: Optional[dict[str, Any]] = None,
1817
- dir_path: Optional[str] = None,
1821
+ input_dir_path: Optional[str] = None,
1822
+ output_dir_path: Optional[str] = ".",
1818
1823
  ) -> RunResult:
1819
1824
  """
1820
1825
  Submit an input to start a new run of the application and poll for the
@@ -1833,14 +1838,14 @@ class Application:
1833
1838
  input data is extracted from the `.data` property.
1834
1839
 
1835
1840
  If you want to work with `nextmv.InputFormat.CSV_ARCHIVE` or
1836
- `nextmv.InputFormat.MULTI_FILE`, you should use the `dir_path`
1841
+ `nextmv.InputFormat.MULTI_FILE`, you should use the `input_dir_path`
1837
1842
  argument instead. This argument takes precedence over the `input`.
1838
- If `dir_path` is specified, this function looks for files in that
1843
+ If `input_dir_path` is specified, this function looks for files in that
1839
1844
  directory and tars them, to later be uploaded using the
1840
- `upload_large_input` method. If both the `dir_path` and `input`
1845
+ `upload_large_input` method. If both the `input_dir_path` and `input`
1841
1846
  arguments are provided, the `input` is ignored.
1842
1847
 
1843
- When `dir_path` is specified, the `configuration` argument must
1848
+ When `input_dir_path` is specified, the `configuration` argument must
1844
1849
  also be provided. More specifically, the
1845
1850
  `RunConfiguration.format.format_input.input_type` parameter
1846
1851
  dictates what kind of input is being submitted to the Nextmv Cloud.
@@ -1897,13 +1902,17 @@ class Application:
1897
1902
  json_configurations: Optional[dict[str, Any]]
1898
1903
  Optional configurations for JSON serialization. This is used to
1899
1904
  customize the serialization before data is sent.
1900
- dir_path: Optional[str]
1905
+ input_dir_path: Optional[str]
1901
1906
  Path to a directory containing input files. If specified, the
1902
1907
  function will package the files in the directory into a tar file
1903
1908
  and upload it as a large input. This is useful for input formats
1904
1909
  like `nextmv.InputFormat.CSV_ARCHIVE` or `nextmv.InputFormat.MULTI_FILE`.
1905
- If both `input` and `dir_path` are specified, the `input` is
1910
+ If both `input` and `input_dir_path` are specified, the `input` is
1906
1911
  ignored, and the files in the directory are used instead.
1912
+ output_dir_path : Optional[str], default="."
1913
+ Path to a directory where non-JSON output files will be saved. This is
1914
+ required if the output is non-JSON. If the directory does not exist, it
1915
+ will be created. Uses the current directory by default.
1907
1916
 
1908
1917
  Returns
1909
1918
  ----------
@@ -1936,12 +1945,13 @@ class Application:
1936
1945
  batch_experiment_id=batch_experiment_id,
1937
1946
  external_result=external_result,
1938
1947
  json_configurations=json_configurations,
1939
- dir_path=dir_path,
1948
+ input_dir_path=input_dir_path,
1940
1949
  )
1941
1950
 
1942
1951
  return self.run_result_with_polling(
1943
1952
  run_id=run_id,
1944
1953
  polling_options=polling_options,
1954
+ output_dir_path=output_dir_path,
1945
1955
  )
1946
1956
 
1947
1957
  def new_scenario_test(
@@ -2215,10 +2225,13 @@ class Application:
2215
2225
  if exist_ok and self.version_exists(version_id=id):
2216
2226
  return self.version(version_id=id)
2217
2227
 
2218
- payload = {}
2228
+ if id is None:
2229
+ id = _safe_id(prefix="version")
2230
+
2231
+ payload = {
2232
+ "id": id,
2233
+ }
2219
2234
 
2220
- if id is not None:
2221
- payload["id"] = id
2222
2235
  if name is not None:
2223
2236
  payload["name"] = name
2224
2237
  if description is not None:
@@ -2501,7 +2514,7 @@ class Application:
2501
2514
  )
2502
2515
  return RunLog.from_dict(response.json())
2503
2516
 
2504
- def run_result(self, run_id: str) -> RunResult:
2517
+ def run_result(self, run_id: str, output_dir_path: Optional[str] = ".") -> RunResult:
2505
2518
  """
2506
2519
  Get the result of a run.
2507
2520
 
@@ -2511,6 +2524,10 @@ class Application:
2511
2524
  ----------
2512
2525
  run_id : str
2513
2526
  ID of the run to get results for.
2527
+ output_dir_path : Optional[str], default="."
2528
+ Path to a directory where non-JSON output files will be saved. This is
2529
+ required if the output is non-JSON. If the directory does not exist, it
2530
+ will be created. Uses the current directory by default.
2514
2531
 
2515
2532
  Returns
2516
2533
  -------
@@ -2531,12 +2548,17 @@ class Application:
2531
2548
 
2532
2549
  run_information = self.run_metadata(run_id=run_id)
2533
2550
 
2534
- return self.__run_result(run_id=run_id, run_information=run_information)
2551
+ return self.__run_result(
2552
+ run_id=run_id,
2553
+ run_information=run_information,
2554
+ output_dir_path=output_dir_path,
2555
+ )
2535
2556
 
2536
2557
  def run_result_with_polling(
2537
2558
  self,
2538
2559
  run_id: str,
2539
2560
  polling_options: PollingOptions = _DEFAULT_POLLING_OPTIONS,
2561
+ output_dir_path: Optional[str] = ".",
2540
2562
  ) -> RunResult:
2541
2563
  """
2542
2564
  Get the result of a run with polling.
@@ -2551,6 +2573,10 @@ class Application:
2551
2573
  ID of the run to retrieve the result for.
2552
2574
  polling_options : PollingOptions, default=_DEFAULT_POLLING_OPTIONS
2553
2575
  Options to use when polling for the run result.
2576
+ output_dir_path : Optional[str], default="."
2577
+ Path to a directory where non-JSON output files will be saved. This is
2578
+ required if the output is non-JSON. If the directory does not exist, it
2579
+ will be created. Uses the current directory by default.
2554
2580
 
2555
2581
  Returns
2556
2582
  -------
@@ -2592,7 +2618,11 @@ class Application:
2592
2618
 
2593
2619
  run_information = poll(polling_options=polling_options, polling_func=polling_func)
2594
2620
 
2595
- return self.__run_result(run_id=run_id, run_information=run_information)
2621
+ return self.__run_result(
2622
+ run_id=run_id,
2623
+ run_information=run_information,
2624
+ output_dir_path=output_dir_path,
2625
+ )
2596
2626
 
2597
2627
  def scenario_test(self, scenario_test_id: str) -> BatchExperiment:
2598
2628
  """
@@ -2707,6 +2737,7 @@ class Application:
2707
2737
  tracked_run: TrackedRun,
2708
2738
  polling_options: PollingOptions = _DEFAULT_POLLING_OPTIONS,
2709
2739
  instance_id: Optional[str] = None,
2740
+ output_dir_path: Optional[str] = ".",
2710
2741
  ) -> RunResult:
2711
2742
  """
2712
2743
  Track an external run and poll for the result. This is a convenience
@@ -2723,6 +2754,10 @@ class Application:
2723
2754
  instance_id: Optional[str]
2724
2755
  Optional instance ID if you want to associate your tracked run with
2725
2756
  an instance.
2757
+ output_dir_path : Optional[str], default="."
2758
+ Path to a directory where non-JSON output files will be saved. This is
2759
+ required if the output is non-JSON. If the directory does not exist, it
2760
+ will be created. Uses the current directory by default.
2726
2761
 
2727
2762
  Returns
2728
2763
  -------
@@ -2747,12 +2782,13 @@ class Application:
2747
2782
  return self.run_result_with_polling(
2748
2783
  run_id=run_id,
2749
2784
  polling_options=polling_options,
2785
+ output_dir_path=output_dir_path,
2750
2786
  )
2751
2787
 
2752
2788
  def update_instance(
2753
2789
  self,
2754
2790
  id: str,
2755
- name: str,
2791
+ name: Optional[str] = None,
2756
2792
  version_id: Optional[str] = None,
2757
2793
  description: Optional[str] = None,
2758
2794
  configuration: Optional[InstanceConfiguration] = None,
@@ -2764,14 +2800,14 @@ class Application:
2764
2800
  ----------
2765
2801
  id : str
2766
2802
  ID of the instance to update.
2767
- name : str
2768
- Name of the instance.
2803
+ name : Optional[str], default=None
2804
+ Optional name of the instance.
2769
2805
  version_id : Optional[str], default=None
2770
- ID of the version to associate the instance with.
2806
+ Optional ID of the version to associate the instance with.
2771
2807
  description : Optional[str], default=None
2772
- Description of the instance.
2808
+ Optional description of the instance.
2773
2809
  configuration : Optional[InstanceConfiguration], default=None
2774
- Configuration to use for the instance.
2810
+ Optional configuration to use for the instance.
2775
2811
 
2776
2812
  Returns
2777
2813
  -------
@@ -2784,12 +2820,21 @@ class Application:
2784
2820
  If the response status code is not 2xx.
2785
2821
  """
2786
2822
 
2787
- payload = {}
2823
+ # Get the instance as it currently exsits.
2824
+ instance = self.instance(id)
2825
+ instance_dict = instance.to_dict()
2826
+
2827
+ payload = {
2828
+ "name": instance_dict["name"],
2829
+ "version_id": instance_dict["version_id"],
2830
+ "description": instance_dict["description"],
2831
+ "configuration": instance_dict["configuration"],
2832
+ }
2788
2833
 
2789
- if version_id is not None:
2790
- payload["version_id"] = version_id
2791
2834
  if name is not None:
2792
2835
  payload["name"] = name
2836
+ if version_id is not None:
2837
+ payload["version_id"] = version_id
2793
2838
  if description is not None:
2794
2839
  payload["description"] = description
2795
2840
  if configuration is not None:
@@ -2806,8 +2851,8 @@ class Application:
2806
2851
  def update_batch_experiment(
2807
2852
  self,
2808
2853
  batch_experiment_id: str,
2809
- name: str,
2810
- description: str,
2854
+ name: Optional[str] = None,
2855
+ description: Optional[str] = None,
2811
2856
  ) -> BatchExperimentInformation:
2812
2857
  """
2813
2858
  Update a batch experiment.
@@ -2816,10 +2861,10 @@ class Application:
2816
2861
  ----------
2817
2862
  batch_experiment_id : str
2818
2863
  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.
2864
+ name : Optional[str], default=None
2865
+ Optional name of the batch experiment.
2866
+ description : Optional[str], default=None
2867
+ Optional description of the batch experiment.
2823
2868
 
2824
2869
  Returns
2825
2870
  -------
@@ -2832,10 +2877,13 @@ class Application:
2832
2877
  If the response status code is not 2xx.
2833
2878
  """
2834
2879
 
2835
- payload = {
2836
- "name": name,
2837
- "description": description,
2838
- }
2880
+ payload = {}
2881
+
2882
+ if name is not None:
2883
+ payload["name"] = name
2884
+ if description is not None:
2885
+ payload["description"] = description
2886
+
2839
2887
  response = self.client.request(
2840
2888
  method="PATCH",
2841
2889
  endpoint=f"{self.experiments_endpoint}/batch/{batch_experiment_id}",
@@ -2847,9 +2895,9 @@ class Application:
2847
2895
  def update_managed_input(
2848
2896
  self,
2849
2897
  managed_input_id: str,
2850
- name: str,
2851
- description: str,
2852
- ) -> None:
2898
+ name: Optional[str] = None,
2899
+ description: Optional[str] = None,
2900
+ ) -> ManagedInput:
2853
2901
  """
2854
2902
  Update a managed input.
2855
2903
 
@@ -2857,15 +2905,15 @@ class Application:
2857
2905
  ----------
2858
2906
  managed_input_id : str
2859
2907
  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.
2908
+ name : Optional[str], default=None
2909
+ Optional new name for the managed input.
2910
+ description : Optional[str], default=None
2911
+ Optional new description for the managed input.
2864
2912
 
2865
2913
  Returns
2866
2914
  -------
2867
- None
2868
- No return value.
2915
+ ManagedInput
2916
+ The updated managed input.
2869
2917
 
2870
2918
  Raises
2871
2919
  ------
@@ -2873,21 +2921,32 @@ class Application:
2873
2921
  If the response status code is not 2xx.
2874
2922
  """
2875
2923
 
2924
+ managed_input = self.managed_input(managed_input_id)
2925
+ managed_input_dict = managed_input.to_dict()
2926
+
2876
2927
  payload = {
2877
- "name": name,
2878
- "description": description,
2928
+ "name": managed_input_dict["name"],
2929
+ "description": managed_input_dict["description"],
2879
2930
  }
2880
- _ = self.client.request(
2931
+
2932
+ if name is not None:
2933
+ payload["name"] = name
2934
+ if description is not None:
2935
+ payload["description"] = description
2936
+
2937
+ response = self.client.request(
2881
2938
  method="PUT",
2882
2939
  endpoint=f"{self.endpoint}/inputs/{managed_input_id}",
2883
2940
  payload=payload,
2884
2941
  )
2885
2942
 
2943
+ return ManagedInput.from_dict(response.json())
2944
+
2886
2945
  def update_scenario_test(
2887
2946
  self,
2888
2947
  scenario_test_id: str,
2889
- name: str,
2890
- description: str,
2948
+ name: Optional[str] = None,
2949
+ description: Optional[str] = None,
2891
2950
  ) -> BatchExperimentInformation:
2892
2951
  """
2893
2952
  Update a scenario test.
@@ -2900,10 +2959,10 @@ class Application:
2900
2959
  ----------
2901
2960
  scenario_test_id : str
2902
2961
  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.
2962
+ name : Optional[str], default=None
2963
+ Optional new name for the scenario test.
2964
+ description : Optional[str], default=None
2965
+ Optional new description for the scenario test.
2907
2966
 
2908
2967
  Returns
2909
2968
  -------
@@ -2935,9 +2994,9 @@ class Application:
2935
2994
  def update_secrets_collection(
2936
2995
  self,
2937
2996
  secrets_collection_id: str,
2938
- name: str,
2939
- description: str,
2940
- secrets: list[Secret],
2997
+ name: Optional[str] = None,
2998
+ description: Optional[str] = None,
2999
+ secrets: Optional[list[Secret]] = None,
2941
3000
  ) -> SecretsCollectionSummary:
2942
3001
  """
2943
3002
  Update a secrets collection.
@@ -2950,13 +3009,13 @@ class Application:
2950
3009
  ----------
2951
3010
  secrets_collection_id : str
2952
3011
  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.
3012
+ name : Optional[str], default=None
3013
+ Optional new name for the secrets collection.
3014
+ description : Optional[str], default=None
3015
+ Optional new description for the secrets collection.
3016
+ secrets : Optional[list[Secret]], default=None
3017
+ Optional list of secrets to update. Each secret should be an
3018
+ instance of the Secret class containing a key and value.
2960
3019
 
2961
3020
  Returns
2962
3021
  -------
@@ -2988,14 +3047,22 @@ class Application:
2988
3047
  'api-secrets'
2989
3048
  """
2990
3049
 
2991
- if len(secrets) == 0:
2992
- raise ValueError("secrets must be provided")
3050
+ collection = self.secrets_collection(secrets_collection_id)
3051
+ collection_dict = collection.to_dict()
2993
3052
 
2994
3053
  payload = {
2995
- "name": name,
2996
- "description": description,
2997
- "secrets": [secret.to_dict() for secret in secrets],
3054
+ "name": collection_dict["name"],
3055
+ "description": collection_dict["description"],
3056
+ "secrets": collection_dict["secrets"],
2998
3057
  }
3058
+
3059
+ if name is not None:
3060
+ payload["name"] = name
3061
+ if description is not None:
3062
+ payload["description"] = description
3063
+ if secrets is not None and len(secrets) > 0:
3064
+ payload["secrets"] = [secret.to_dict() for secret in secrets]
3065
+
2999
3066
  response = self.client.request(
3000
3067
  method="PUT",
3001
3068
  endpoint=f"{self.endpoint}/secrets/{secrets_collection_id}",
@@ -3229,6 +3296,7 @@ class Application:
3229
3296
  self,
3230
3297
  run_id: str,
3231
3298
  run_information: RunInformation,
3299
+ output_dir_path: Optional[str] = ".",
3232
3300
  ) -> RunResult:
3233
3301
  """
3234
3302
  Get the result of a run.
@@ -3245,6 +3313,10 @@ class Application:
3245
3313
  ID of the run to retrieve the result for.
3246
3314
  run_information : RunInformation
3247
3315
  Information about the run, including metadata such as output size.
3316
+ output_dir_path : Optional[str], default="."
3317
+ Path to a directory where non-JSON output files will be saved. This is
3318
+ required if the output is non-JSON. If the directory does not exist, it
3319
+ will be created. Uses the current directory by default.
3248
3320
 
3249
3321
  Returns
3250
3322
  -------
@@ -3265,10 +3337,13 @@ class Application:
3265
3337
  a download URL and fetch the output data separately.
3266
3338
  """
3267
3339
  query_params = None
3268
- large_output = False
3269
- if run_information.metadata.output_size > _MAX_RUN_SIZE:
3340
+ use_presigned_url = False
3341
+ if (
3342
+ run_information.metadata.format.format_output.output_type != OutputFormat.JSON
3343
+ or run_information.metadata.output_size > _MAX_RUN_SIZE
3344
+ ):
3270
3345
  query_params = {"format": "url"}
3271
- large_output = True
3346
+ use_presigned_url = True
3272
3347
 
3273
3348
  response = self.client.request(
3274
3349
  method="GET",
@@ -3278,7 +3353,7 @@ class Application:
3278
3353
  result = RunResult.from_dict(response.json())
3279
3354
  result.console_url = self.__console_url(result.id)
3280
3355
 
3281
- if not large_output:
3356
+ if not use_presigned_url or result.metadata.status_v2 != StatusV2.succeeded:
3282
3357
  return result
3283
3358
 
3284
3359
  download_url = DownloadURL.from_dict(response.json()["output"])
@@ -3287,7 +3362,24 @@ class Application:
3287
3362
  endpoint=download_url.url,
3288
3363
  headers={"Content-Type": "application/json"},
3289
3364
  )
3290
- result.output = download_response.json()
3365
+
3366
+ # See whether we can attach the output directly or need to save to the given
3367
+ # directory
3368
+ if run_information.metadata.format.format_output.output_type != OutputFormat.JSON:
3369
+ if not output_dir_path or output_dir_path == "":
3370
+ raise ValueError(
3371
+ "If the output format is not JSON, an output_dir_path must be provided.",
3372
+ )
3373
+ if not os.path.exists(output_dir_path):
3374
+ os.makedirs(output_dir_path, exist_ok=True)
3375
+ # Save .tar.gz file to a temp directory and extract contents to output_dir_path
3376
+ with tempfile.TemporaryDirectory() as tmpdirname:
3377
+ temp_tar_path = os.path.join(tmpdirname, f"{run_id}.tar.gz")
3378
+ with open(temp_tar_path, "wb") as f:
3379
+ f.write(download_response.content)
3380
+ shutil.unpack_archive(temp_tar_path, output_dir_path)
3381
+ else:
3382
+ result.output = download_response.json()
3291
3383
 
3292
3384
  return result
3293
3385
 
@@ -3325,7 +3417,14 @@ class Application:
3325
3417
  }
3326
3418
 
3327
3419
  if manifest.configuration is not None and manifest.configuration.options is not None:
3328
- activation_request["requirements"]["options"] = manifest.configuration.options.to_dict()
3420
+ options = manifest.configuration.options.to_dict()
3421
+ if "format" in options and isinstance(options["format"], list):
3422
+ # the endpoint expects a dictionary with a template key having a list of strings
3423
+ # the app.yaml however defines format as a list of strings, so we need to convert it here
3424
+ options["format"] = {
3425
+ "template": options["format"],
3426
+ }
3427
+ activation_request["requirements"]["options"] = options
3329
3428
 
3330
3429
  response = self.client.request(
3331
3430
  method="PUT",
nextmv/cloud/manifest.py CHANGED
@@ -347,6 +347,9 @@ class ManifestOptionUI(BaseModel):
347
347
  A list of team roles to which this option will be hidden in the UI. For
348
348
  example, if you want to hide an option from the "operator" role, you can
349
349
  pass `hidden_from=["operator"]`.
350
+ display_name : str, optional
351
+ An optional display name for the option. This is useful for making
352
+ the option more user-friendly in the UI.
350
353
 
351
354
  Examples
352
355
  --------
@@ -360,6 +363,10 @@ class ManifestOptionUI(BaseModel):
360
363
  """The type of control to use for the option in the Nextmv Cloud UI."""
361
364
  hidden_from: Optional[list[str]] = None
362
365
  """A list of team roles for which this option will be hidden in the UI."""
366
+ display_name: Optional[str] = None
367
+ """An optional display name for the option. This is useful for making
368
+ the option more user-friendly in the UI.
369
+ """
363
370
 
364
371
 
365
372
  class ManifestOption(BaseModel):
@@ -484,8 +491,9 @@ class ManifestOption(BaseModel):
484
491
  ui=ManifestOptionUI(
485
492
  control_type=option.control_type,
486
493
  hidden_from=option.hidden_from,
494
+ display_name=option.display_name,
487
495
  )
488
- if option.control_type or option.hidden_from
496
+ if option.control_type or option.hidden_from or option.display_name
489
497
  else None,
490
498
  )
491
499
 
@@ -535,6 +543,7 @@ class ManifestOption(BaseModel):
535
543
  additional_attributes=self.additional_attributes,
536
544
  control_type=self.ui.control_type if self.ui else None,
537
545
  hidden_from=self.ui.hidden_from if self.ui else None,
546
+ display_name=self.ui.display_name if self.ui else None,
538
547
  )
539
548
 
540
549
 
@@ -593,6 +602,10 @@ class ManifestOptions(BaseModel):
593
602
  is a parameter that configures the decision model.
594
603
  validation: Optional[ManifestValidation], default=None
595
604
  Optional validation rules for all options.
605
+ format: Optional[list[str]], default=None
606
+ A list of strings that define how options are transformed into command
607
+ line arguments. Use `{{name}}` to refer to the option name and
608
+ `{{value}}` to refer to the option value.
596
609
 
597
610
 
598
611
  Examples
@@ -621,9 +634,21 @@ class ManifestOptions(BaseModel):
621
634
 
622
635
  An option is a parameter that configures the decision model.
623
636
  """
637
+ format: Optional[list[str]] = None
638
+ """A list of strings that define how options are transformed into command line arguments.
639
+
640
+ Use `{{name}}` to refer to the option name and `{{value}}` to refer to the option value.
641
+ For example, `["-{{name}}", "{{value}}"]` will transform an option named `max_vehicles`
642
+ with a value of `10` into the command line argument `-max_vehicles 10`.
643
+ """
624
644
 
625
645
  @classmethod
626
- def from_options(cls, options: Options, validation: OptionsEnforcement = None) -> "ManifestOptions":
646
+ def from_options(
647
+ cls,
648
+ options: Options,
649
+ validation: OptionsEnforcement = None,
650
+ format: Optional[list[str]] = None,
651
+ ) -> "ManifestOptions":
627
652
  """
628
653
  Create a `ManifestOptions` from a `nextmv.Options`.
629
654
 
@@ -634,6 +659,14 @@ class ManifestOptions(BaseModel):
634
659
  validation : Optional[OptionsEnforcement], default=None
635
660
  Optional validation rules for the options. If provided, it will be
636
661
  used to set the `validation` attribute of the `ManifestOptions`.
662
+ format : Optional[list[str]], default=None
663
+ A list of strings that define how options are transformed into
664
+ command line arguments. Use `{{name}}` to refer to the option name
665
+ and `{{value}}` to refer to the option value.
666
+
667
+ For example, `["-{{name}}", "{{value}}"]` will transform an option
668
+ named `max_vehicles` with a value of `10` into the command line
669
+ argument `-max_vehicles 10`.
637
670
 
638
671
  Returns
639
672
  -------
@@ -655,6 +688,7 @@ class ManifestOptions(BaseModel):
655
688
  strict=validation.strict if validation else False,
656
689
  validation=ManifestValidation(enforce="all" if validation and validation.validation_enforce else "none"),
657
690
  items=items,
691
+ format=format,
658
692
  )
659
693
 
660
694
 
nextmv/cloud/run.py CHANGED
@@ -14,6 +14,8 @@ RunLog
14
14
  Log of a run.
15
15
  FormatInput
16
16
  Input format for a run configuration.
17
+ FormatOutput
18
+ Output format for a run configuration.
17
19
  Format
18
20
  Format for a run configuration.
19
21
  RunType
@@ -110,6 +112,83 @@ def run_duration(start: Union[datetime, float], end: Union[datetime, float]) ->
110
112
  raise TypeError("Start and end must be either datetime or float.")
111
113
 
112
114
 
115
+ class FormatInput(BaseModel):
116
+ """
117
+ Input format for a run configuration.
118
+
119
+ You can import the `FormatInput` class directly from `cloud`:
120
+
121
+ ```python
122
+ from nextmv.cloud import FormatInput
123
+ ```
124
+
125
+ Parameters
126
+ ----------
127
+ input_type : InputFormat, optional
128
+ Type of the input format. Defaults to `InputFormat.JSON`.
129
+ """
130
+
131
+ input_type: InputFormat = Field(
132
+ serialization_alias="type",
133
+ validation_alias=AliasChoices("type", "input_type"),
134
+ default=InputFormat.JSON,
135
+ )
136
+ """Type of the input format."""
137
+
138
+
139
+ class FormatOutput(BaseModel):
140
+ """
141
+ Output format for a run configuration.
142
+
143
+ You can import the `FormatOutput` class directly from `cloud`:
144
+
145
+ ```python
146
+ from nextmv.cloud import FormatOutput
147
+ ```
148
+
149
+ Parameters
150
+ ----------
151
+ output_type : OutputFormat, optional
152
+ Type of the output format. Defaults to `OutputFormat.JSON`.
153
+ """
154
+
155
+ output_type: OutputFormat = Field(
156
+ serialization_alias="type",
157
+ validation_alias=AliasChoices("type", "output_type"),
158
+ default=OutputFormat.JSON,
159
+ )
160
+ """Type of the output format."""
161
+
162
+
163
+ class Format(BaseModel):
164
+ """
165
+ Format for a run configuration.
166
+
167
+ You can import the `Format` class directly from `cloud`:
168
+
169
+ ```python
170
+ from nextmv.cloud import Format
171
+ ```
172
+
173
+ Parameters
174
+ ----------
175
+ format_input : FormatInput
176
+ Input format for the run configuration.
177
+ """
178
+
179
+ format_input: FormatInput = Field(
180
+ serialization_alias="input",
181
+ validation_alias=AliasChoices("input", "format_input"),
182
+ )
183
+ """Input format for the run configuration."""
184
+ format_output: Optional[FormatOutput] = Field(
185
+ serialization_alias="output",
186
+ validation_alias=AliasChoices("output", "format_output"),
187
+ default=None,
188
+ )
189
+ """Output format for the run configuration."""
190
+
191
+
113
192
  class Metadata(BaseModel):
114
193
  """
115
194
  Metadata of a run, whether it was successful or not.
@@ -160,6 +239,8 @@ class Metadata(BaseModel):
160
239
  """Size of the input in bytes."""
161
240
  output_size: float
162
241
  """Size of the output in bytes."""
242
+ format: Format
243
+ """Format of the input and output of the run."""
163
244
  status: Status
164
245
  """Deprecated: use status_v2."""
165
246
  status_v2: StatusV2
@@ -279,53 +360,6 @@ class RunLog(BaseModel):
279
360
  """Log of the run."""
280
361
 
281
362
 
282
- class FormatInput(BaseModel):
283
- """
284
- Input format for a run configuration.
285
-
286
- You can import the `FormatInput` class directly from `cloud`:
287
-
288
- ```python
289
- from nextmv.cloud import FormatInput
290
- ```
291
-
292
- Parameters
293
- ----------
294
- input_type : InputFormat, optional
295
- Type of the input format. Defaults to `InputFormat.JSON`.
296
- """
297
-
298
- input_type: InputFormat = Field(
299
- serialization_alias="type",
300
- validation_alias=AliasChoices("type", "input_type"),
301
- default=InputFormat.JSON,
302
- )
303
- """Type of the input format."""
304
-
305
-
306
- class Format(BaseModel):
307
- """
308
- Format for a run configuration.
309
-
310
- You can import the `Format` class directly from `cloud`:
311
-
312
- ```python
313
- from nextmv.cloud import Format
314
- ```
315
-
316
- Parameters
317
- ----------
318
- format_input : FormatInput
319
- Input format for the run configuration.
320
- """
321
-
322
- format_input: FormatInput = Field(
323
- serialization_alias="input",
324
- validation_alias=AliasChoices("input", "format_input"),
325
- )
326
- """Input format for the run configuration."""
327
-
328
-
329
363
  class RunType(str, Enum):
330
364
  """
331
365
  The actual type of the run.
@@ -491,7 +525,10 @@ class RunConfiguration(BaseModel):
491
525
  if self.format is not None:
492
526
  return
493
527
 
494
- self.format = Format(format_input=FormatInput(input_type=InputFormat.JSON))
528
+ self.format = Format(
529
+ format_input=FormatInput(input_type=InputFormat.JSON),
530
+ format_output=FormatOutput(output_type=OutputFormat.JSON),
531
+ )
495
532
 
496
533
  if isinstance(input, dict):
497
534
  self.format.format_input.input_type = InputFormat.JSON
@@ -504,6 +541,19 @@ class RunConfiguration(BaseModel):
504
541
  elif isinstance(input, Input):
505
542
  self.format.format_input.input_type = input.input_format
506
543
 
544
+ # As input and output are symmetric, we set the output according to the input
545
+ # format.
546
+ if self.format.format_input.input_type == InputFormat.JSON:
547
+ self.format.format_output = FormatOutput(output_type=OutputFormat.JSON)
548
+ elif self.format.format_input.input_type == InputFormat.TEXT: # Text still maps to json
549
+ self.format.format_output = FormatOutput(output_type=OutputFormat.JSON)
550
+ elif self.format.format_input.input_type == InputFormat.CSV_ARCHIVE:
551
+ self.format.format_output = FormatOutput(output_type=OutputFormat.CSV_ARCHIVE)
552
+ elif self.format.format_input.input_type == InputFormat.MULTI_FILE:
553
+ self.format.format_output = FormatOutput(output_type=OutputFormat.MULTI_FILE)
554
+ else:
555
+ self.format.format_output = FormatOutput(output_type=OutputFormat.JSON)
556
+
507
557
 
508
558
  class ExternalRunResult(BaseModel):
509
559
  """
nextmv/cloud/safe.py CHANGED
@@ -81,3 +81,33 @@ def _name_and_id(prefix: str, entity_id: str) -> tuple[str, str]:
81
81
  safe_name = _start_case(safe_id)
82
82
 
83
83
  return safe_name, safe_id
84
+
85
+
86
+ def _safe_id(prefix: str) -> str:
87
+ """
88
+ Generate a safe ID from a prefix.
89
+
90
+ Parameters
91
+ ----------
92
+ prefix : str
93
+ Prefix to use for the ID.
94
+
95
+ Returns
96
+ -------
97
+ str
98
+ A safe ID.
99
+ """
100
+
101
+ random_slug = _nanoid(8)
102
+ # Space available for user text once prefix, random slug and separator "-"
103
+ # are accounted for
104
+ safe_id_max = (
105
+ ENTITY_ID_CHAR_COUNT_MAX - INDEX_TAG_CHAR_COUNT - (len(random_slug) + 1) # +1 for the hyphen before the slug
106
+ )
107
+
108
+ if len(prefix) > safe_id_max:
109
+ return prefix[: safe_id_max - 1] + f"-{random_slug}"
110
+
111
+ safe_id = f"{prefix}-{random_slug}"
112
+
113
+ return safe_id
nextmv/model.py CHANGED
@@ -160,7 +160,6 @@ class ModelConfiguration:
160
160
  """Enforcement of options for the model."""
161
161
 
162
162
 
163
-
164
163
  class Model:
165
164
  """
166
165
  Base class for defining decision models that run in Nextmv Cloud.
nextmv/options.py CHANGED
@@ -227,6 +227,9 @@ class Option:
227
227
  A list of team roles to which this option will be hidden in the UI. For
228
228
  example, if you want to hide an option from the "operator" role, you can
229
229
  pass `hidden_from=["operator"]`.
230
+ display_name : str, optional
231
+ An optional display name for the option. This is useful for making
232
+ the option more user-friendly in the UI.
230
233
 
231
234
  Examples
232
235
  --------
@@ -284,6 +287,11 @@ class Option:
284
287
  example, if you want to hide an option from the "operator" role, you can
285
288
  pass `hidden_from=["operator"]`.
286
289
  """
290
+ display_name: Optional[str] = None
291
+ """
292
+ An optional display name for the option. This is useful for making
293
+ the option more user-friendly in the UI.
294
+ """
287
295
 
288
296
  @classmethod
289
297
  def from_dict(cls, data: dict[str, Any]) -> "Option":
@@ -324,6 +332,7 @@ class Option:
324
332
  additional_attributes=data.get("additional_attributes"),
325
333
  control_type=data.get("control_type"),
326
334
  hidden_from=data.get("hidden_from"),
335
+ display_name=data.get("display_name"),
327
336
  )
328
337
 
329
338
  def to_dict(self) -> dict[str, Any]:
@@ -355,6 +364,7 @@ class Option:
355
364
  "additional_attributes": self.additional_attributes,
356
365
  "control_type": self.control_type,
357
366
  "hidden_from": self.hidden_from,
367
+ "display_name": self.display_name,
358
368
  }
359
369
 
360
370
 
@@ -980,6 +990,9 @@ class Options:
980
990
  if isinstance(option, Option) and option.hidden_from:
981
991
  description += f" (hidden from: {', '.join(option.hidden_from)})"
982
992
 
993
+ if isinstance(option, Option) and option.display_name is not None:
994
+ description += f" (display name: {option.display_name})"
995
+
983
996
  if option.description is not None and option.description != "":
984
997
  description += f": {option.description}"
985
998
 
@@ -1051,6 +1064,7 @@ class Options:
1051
1064
  else:
1052
1065
  raise TypeError(f"expected an <Option> (or deprecated <Parameter>) object, but got {type(option)}")
1053
1066
 
1067
+
1054
1068
  class OptionsEnforcement:
1055
1069
  """
1056
1070
  OptionsEnforcement is a class that provides rules for how the options
@@ -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.dev0
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/
@@ -1,4 +1,4 @@
1
- nextmv/__about__.py,sha256=M8JE5o9w_sW2kAiXxbuOD5CVtJuSX6YEGncr8aePGdY,30
1
+ nextmv/__about__.py,sha256=fH2N4VgXGHNLupMl8aEtSxm93zKJU7HumfTMlmPMKeE,30
2
2
  nextmv/__entrypoint__.py,sha256=dA0iwwHtrq6Z9w9FxmxKLoBGLyhe7jWtUAU-Y3PEgHg,1094
3
3
  nextmv/__init__.py,sha256=FsF0pEkOSBuPY5EKu7NsBxro7jswGmOmaw61kZEudXY,1930
4
4
  nextmv/_serialization.py,sha256=JlSl6BL0M2Esf7F89GsGIZ__Pp8RnFRNM0UxYhuuYU4,2853
@@ -6,21 +6,21 @@ nextmv/base_model.py,sha256=qmJ4AsYr9Yv01HQX_BERrn3229gyoZrYyP9tcyqNfeU,2311
6
6
  nextmv/deprecated.py,sha256=kEVfyQ-nT0v2ePXTNldjQG9uH5IlfQVy3L4tztIxwmU,1638
7
7
  nextmv/input.py,sha256=iTMIdhSi4H-Xot44CYaUH110WDcpWDsJ5JXxSMGIZaY,40030
8
8
  nextmv/logger.py,sha256=kNIbu46MisrzYe4T0hNMpWfRTKKacDVvbtQcNys_c_E,2513
9
- nextmv/model.py,sha256=SVoJLN_f5knXjaPLlyWWvzIqX_qKu1txn6pYfKPwt14,15019
10
- nextmv/options.py,sha256=-9ru7nzqMsFf0aAAfR5OuMvChXYT6aho5sGghHKu8Ds,37323
9
+ nextmv/model.py,sha256=vI3pSV3iTwjRPflar7nAg-6h98XRUyi9II5O2J06-Kc,15018
10
+ nextmv/options.py,sha256=yPJu5lYMbV6YioMwAXv7ctpZUggLXKlZc9CqIbUFvE4,37895
11
11
  nextmv/output.py,sha256=vcBqtP1hJLYyUvfk_vl1Ejbk3q5KxOmlTz4Lnqqnyd0,54141
12
- nextmv/cloud/__init__.py,sha256=7BCh3z-XkbIcMvFHmbj2wA8OquIovjrAZL7O9kA9VZc,3868
12
+ nextmv/cloud/__init__.py,sha256=cGRkIcn1evTdgw7lEH2k4MPTZnq37XIF3ovbuDjUWGI,3914
13
13
  nextmv/cloud/acceptance_test.py,sha256=Bcfdmh2fkPeBx8FDCngeUo2fjV_LhsUdygnzDQCDbYY,26898
14
14
  nextmv/cloud/account.py,sha256=eukiYQha4U2fkIjg4SgdoawKE1kU5G7GPyDJVrn8hHA,6064
15
- nextmv/cloud/application.py,sha256=kWmmufaNP7t5ijaliiJTJ1Bt5URx3ETJOAk80tOgrEc,124634
15
+ nextmv/cloud/application.py,sha256=P5LYXNYlaJYTZiCbJxxG3pvT6KLHS8ZK8HO1R46Lju4,130127
16
16
  nextmv/cloud/batch_experiment.py,sha256=Rmcwe1uAVz2kRrAMQqLYC5d__L_IqPXbF__RE3uQTAY,8196
17
17
  nextmv/cloud/client.py,sha256=E0DiUb377jvEnpXlRnfT1PGCI0Jm0lTUoX5VqeU91lk,18165
18
18
  nextmv/cloud/input_set.py,sha256=2dqmf5z-rZjTKwtBRvnUdfPfKv28It5uTCX0C70uP4Y,4242
19
19
  nextmv/cloud/instance.py,sha256=SS4tbp0LQMWDaeYpwcNxJei82oi_Hozv1t5i3QGjASY,4024
20
- nextmv/cloud/manifest.py,sha256=k_i_KlQ33Lsz9hhirWDRbXMdyEh-aM-4aWOhIBhiGAA,35781
20
+ nextmv/cloud/manifest.py,sha256=5FqXtT1q7qmyQxqB5rG33y97WFZ6erL1f9kRel575RQ,37456
21
21
  nextmv/cloud/package.py,sha256=f0OjdlIOsI2LpmgSxdFf6YaA8Ucs9yAm_3bO0Cp8LH4,13027
22
- nextmv/cloud/run.py,sha256=YPBVjbnc6Ebgjxm5Rw1eY2-MiYx3KC7fQyqKWXY9auY,20836
23
- nextmv/cloud/safe.py,sha256=idifvV8P_79Zo2hIC_qxqZt9LUmD5TLQ9ikKwRUvd34,2522
22
+ nextmv/cloud/run.py,sha256=-I1S5fLIFLH1P9xViv5bOS2LbO9-w6tnB8cBGU0_yTo,22743
23
+ nextmv/cloud/safe.py,sha256=hWh_quD2KSazTV5SOAddOsFLsY4oWBzajOUcCeHOjc8,3180
24
24
  nextmv/cloud/scenario.py,sha256=JRFTDiFBcrgud6wE2qDHUu5oO-Ur3zbPYhhB6ONCxTo,14263
25
25
  nextmv/cloud/secrets.py,sha256=fA5cX0jfTsPVZWV7433wzETGlXpWRLHGswuObx9e6FQ,6820
26
26
  nextmv/cloud/status.py,sha256=blvykRCTCTBkaqH88j4dzdQLhU2v1Ig62-_va98zw20,2789
@@ -31,7 +31,7 @@ nextmv/default_app/requirements.txt,sha256=wRE_HkYYWzCGnYZ2NuatHXul4gCHvU3iUAdsx
31
31
  nextmv/default_app/src/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  nextmv/default_app/src/main.py,sha256=HlO8UwZbZYiAQyrZDSR_T4NBcwP1gIjdTdwkJP_dn8I,847
33
33
  nextmv/default_app/src/visuals.py,sha256=WYK_YBnLmYo3TpVev1CpoNCuW5R7hk9QIkeCmvMn1Fs,1014
34
- nextmv-0.29.4.dev1.dist-info/METADATA,sha256=TqdIxsG4oSCF52TvDK7Jl2wSJYbQORtIPfKYTzPNGZA,15831
35
- nextmv-0.29.4.dev1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
36
- nextmv-0.29.4.dev1.dist-info/licenses/LICENSE,sha256=ZIbK-sSWA-OZprjNbmJAglYRtl5_K4l9UwAV3PGJAPc,11349
37
- nextmv-0.29.4.dev1.dist-info/RECORD,,
34
+ nextmv-0.29.5.dev0.dist-info/METADATA,sha256=JOL30ADklPJ0SrmbxPzQvq1J8x15ETOJJU1H5J6amHg,15831
35
+ nextmv-0.29.5.dev0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
36
+ nextmv-0.29.5.dev0.dist-info/licenses/LICENSE,sha256=ZIbK-sSWA-OZprjNbmJAglYRtl5_K4l9UwAV3PGJAPc,11349
37
+ nextmv-0.29.5.dev0.dist-info/RECORD,,