nextmv 0.39.0.dev1__py3-none-any.whl → 1.0.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.
Files changed (161) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__entrypoint__.py +1 -2
  3. nextmv/__init__.py +2 -4
  4. nextmv/cli/CONTRIBUTING.md +583 -0
  5. nextmv/cli/cloud/__init__.py +49 -0
  6. nextmv/cli/cloud/acceptance/__init__.py +27 -0
  7. nextmv/cli/cloud/acceptance/create.py +391 -0
  8. nextmv/cli/cloud/acceptance/delete.py +64 -0
  9. nextmv/cli/cloud/acceptance/get.py +103 -0
  10. nextmv/cli/cloud/acceptance/list.py +62 -0
  11. nextmv/cli/cloud/acceptance/update.py +95 -0
  12. nextmv/cli/cloud/account/__init__.py +28 -0
  13. nextmv/cli/cloud/account/create.py +83 -0
  14. nextmv/cli/cloud/account/delete.py +59 -0
  15. nextmv/cli/cloud/account/get.py +66 -0
  16. nextmv/cli/cloud/account/update.py +70 -0
  17. nextmv/cli/cloud/app/__init__.py +35 -0
  18. nextmv/cli/cloud/app/create.py +140 -0
  19. nextmv/cli/cloud/app/delete.py +57 -0
  20. nextmv/cli/cloud/app/exists.py +44 -0
  21. nextmv/cli/cloud/app/get.py +66 -0
  22. nextmv/cli/cloud/app/list.py +61 -0
  23. nextmv/cli/cloud/app/push.py +432 -0
  24. nextmv/cli/cloud/app/update.py +124 -0
  25. nextmv/cli/cloud/batch/__init__.py +29 -0
  26. nextmv/cli/cloud/batch/create.py +452 -0
  27. nextmv/cli/cloud/batch/delete.py +64 -0
  28. nextmv/cli/cloud/batch/get.py +104 -0
  29. nextmv/cli/cloud/batch/list.py +63 -0
  30. nextmv/cli/cloud/batch/metadata.py +66 -0
  31. nextmv/cli/cloud/batch/update.py +95 -0
  32. nextmv/cli/cloud/data/__init__.py +26 -0
  33. nextmv/cli/cloud/data/upload.py +162 -0
  34. nextmv/cli/cloud/ensemble/__init__.py +33 -0
  35. nextmv/cli/cloud/ensemble/create.py +413 -0
  36. nextmv/cli/cloud/ensemble/delete.py +63 -0
  37. nextmv/cli/cloud/ensemble/get.py +65 -0
  38. nextmv/cli/cloud/ensemble/list.py +63 -0
  39. nextmv/cli/cloud/ensemble/update.py +103 -0
  40. nextmv/cli/cloud/input_set/__init__.py +32 -0
  41. nextmv/cli/cloud/input_set/create.py +168 -0
  42. nextmv/cli/cloud/input_set/delete.py +64 -0
  43. nextmv/cli/cloud/input_set/get.py +63 -0
  44. nextmv/cli/cloud/input_set/list.py +63 -0
  45. nextmv/cli/cloud/input_set/update.py +123 -0
  46. nextmv/cli/cloud/instance/__init__.py +35 -0
  47. nextmv/cli/cloud/instance/create.py +289 -0
  48. nextmv/cli/cloud/instance/delete.py +61 -0
  49. nextmv/cli/cloud/instance/exists.py +39 -0
  50. nextmv/cli/cloud/instance/get.py +62 -0
  51. nextmv/cli/cloud/instance/list.py +60 -0
  52. nextmv/cli/cloud/instance/update.py +216 -0
  53. nextmv/cli/cloud/managed_input/__init__.py +31 -0
  54. nextmv/cli/cloud/managed_input/create.py +144 -0
  55. nextmv/cli/cloud/managed_input/delete.py +64 -0
  56. nextmv/cli/cloud/managed_input/get.py +63 -0
  57. nextmv/cli/cloud/managed_input/list.py +60 -0
  58. nextmv/cli/cloud/managed_input/update.py +97 -0
  59. nextmv/cli/cloud/run/__init__.py +37 -0
  60. nextmv/cli/cloud/run/cancel.py +37 -0
  61. nextmv/cli/cloud/run/create.py +524 -0
  62. nextmv/cli/cloud/run/get.py +199 -0
  63. nextmv/cli/cloud/run/input.py +86 -0
  64. nextmv/cli/cloud/run/list.py +80 -0
  65. nextmv/cli/cloud/run/logs.py +166 -0
  66. nextmv/cli/cloud/run/metadata.py +67 -0
  67. nextmv/cli/cloud/run/track.py +500 -0
  68. nextmv/cli/cloud/scenario/__init__.py +29 -0
  69. nextmv/cli/cloud/scenario/create.py +451 -0
  70. nextmv/cli/cloud/scenario/delete.py +61 -0
  71. nextmv/cli/cloud/scenario/get.py +102 -0
  72. nextmv/cli/cloud/scenario/list.py +63 -0
  73. nextmv/cli/cloud/scenario/metadata.py +67 -0
  74. nextmv/cli/cloud/scenario/update.py +93 -0
  75. nextmv/cli/cloud/secrets/__init__.py +33 -0
  76. nextmv/cli/cloud/secrets/create.py +206 -0
  77. nextmv/cli/cloud/secrets/delete.py +63 -0
  78. nextmv/cli/cloud/secrets/get.py +66 -0
  79. nextmv/cli/cloud/secrets/list.py +60 -0
  80. nextmv/cli/cloud/secrets/update.py +144 -0
  81. nextmv/cli/cloud/shadow/__init__.py +33 -0
  82. nextmv/cli/cloud/shadow/create.py +184 -0
  83. nextmv/cli/cloud/shadow/delete.py +64 -0
  84. nextmv/cli/cloud/shadow/get.py +61 -0
  85. nextmv/cli/cloud/shadow/list.py +63 -0
  86. nextmv/cli/cloud/shadow/metadata.py +66 -0
  87. nextmv/cli/cloud/shadow/start.py +43 -0
  88. nextmv/cli/cloud/shadow/stop.py +53 -0
  89. nextmv/cli/cloud/shadow/update.py +96 -0
  90. nextmv/cli/cloud/switchback/__init__.py +33 -0
  91. nextmv/cli/cloud/switchback/create.py +151 -0
  92. nextmv/cli/cloud/switchback/delete.py +64 -0
  93. nextmv/cli/cloud/switchback/get.py +62 -0
  94. nextmv/cli/cloud/switchback/list.py +63 -0
  95. nextmv/cli/cloud/switchback/metadata.py +68 -0
  96. nextmv/cli/cloud/switchback/start.py +43 -0
  97. nextmv/cli/cloud/switchback/stop.py +53 -0
  98. nextmv/cli/cloud/switchback/update.py +96 -0
  99. nextmv/cli/cloud/upload/__init__.py +22 -0
  100. nextmv/cli/cloud/upload/create.py +39 -0
  101. nextmv/cli/cloud/version/__init__.py +33 -0
  102. nextmv/cli/cloud/version/create.py +96 -0
  103. nextmv/cli/cloud/version/delete.py +61 -0
  104. nextmv/cli/cloud/version/exists.py +39 -0
  105. nextmv/cli/cloud/version/get.py +62 -0
  106. nextmv/cli/cloud/version/list.py +60 -0
  107. nextmv/cli/cloud/version/update.py +92 -0
  108. nextmv/cli/community/__init__.py +24 -0
  109. nextmv/cli/community/clone.py +86 -0
  110. nextmv/cli/community/list.py +200 -0
  111. nextmv/cli/configuration/__init__.py +23 -0
  112. nextmv/cli/configuration/config.py +228 -0
  113. nextmv/cli/configuration/create.py +94 -0
  114. nextmv/cli/configuration/delete.py +67 -0
  115. nextmv/cli/configuration/list.py +77 -0
  116. nextmv/cli/confirm.py +34 -0
  117. nextmv/cli/main.py +161 -3
  118. nextmv/cli/message.py +170 -0
  119. nextmv/cli/options.py +220 -0
  120. nextmv/cli/version.py +22 -2
  121. nextmv/cloud/__init__.py +17 -38
  122. nextmv/cloud/acceptance_test.py +20 -83
  123. nextmv/cloud/account.py +269 -30
  124. nextmv/cloud/application/__init__.py +898 -0
  125. nextmv/cloud/application/_acceptance.py +424 -0
  126. nextmv/cloud/application/_batch_scenario.py +845 -0
  127. nextmv/cloud/application/_ensemble.py +251 -0
  128. nextmv/cloud/application/_input_set.py +263 -0
  129. nextmv/cloud/application/_instance.py +289 -0
  130. nextmv/cloud/application/_managed_input.py +227 -0
  131. nextmv/cloud/application/_run.py +1393 -0
  132. nextmv/cloud/application/_secrets.py +294 -0
  133. nextmv/cloud/application/_shadow.py +320 -0
  134. nextmv/cloud/application/_switchback.py +332 -0
  135. nextmv/cloud/application/_utils.py +54 -0
  136. nextmv/cloud/application/_version.py +304 -0
  137. nextmv/cloud/batch_experiment.py +6 -2
  138. nextmv/cloud/community.py +446 -0
  139. nextmv/cloud/instance.py +11 -1
  140. nextmv/cloud/integration.py +8 -5
  141. nextmv/cloud/package.py +50 -9
  142. nextmv/cloud/shadow.py +254 -0
  143. nextmv/cloud/switchback.py +228 -0
  144. nextmv/deprecated.py +5 -3
  145. nextmv/input.py +20 -88
  146. nextmv/local/application.py +3 -15
  147. nextmv/local/runner.py +1 -1
  148. nextmv/model.py +50 -11
  149. nextmv/options.py +11 -256
  150. nextmv/output.py +0 -62
  151. nextmv/polling.py +54 -16
  152. nextmv/run.py +84 -37
  153. nextmv/status.py +1 -51
  154. {nextmv-0.39.0.dev1.dist-info → nextmv-1.0.0.dist-info}/METADATA +37 -11
  155. nextmv-1.0.0.dist-info/RECORD +185 -0
  156. nextmv-1.0.0.dist-info/entry_points.txt +2 -0
  157. nextmv/cloud/application.py +0 -4204
  158. nextmv-0.39.0.dev1.dist-info/RECORD +0 -55
  159. nextmv-0.39.0.dev1.dist-info/entry_points.txt +0 -2
  160. {nextmv-0.39.0.dev1.dist-info → nextmv-1.0.0.dist-info}/WHEEL +0 -0
  161. {nextmv-0.39.0.dev1.dist-info → nextmv-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,251 @@
1
+ """
2
+ Application mixin for managing app ensembles.
3
+ """
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ from nextmv.cloud.ensemble import EnsembleDefinition, EvaluationRule, RunGroup
8
+ from nextmv.deprecated import deprecated
9
+ from nextmv.safe import safe_id
10
+
11
+ if TYPE_CHECKING:
12
+ from . import Application
13
+
14
+
15
+ class ApplicationEnsembleMixin:
16
+ """
17
+ Mixin class for managing app ensembles within an application.
18
+ """
19
+
20
+ def delete_ensemble_definition(self: "Application", ensemble_definition_id: str) -> None:
21
+ """
22
+ Delete an ensemble definition.
23
+
24
+ Parameters
25
+ ----------
26
+ ensemble_definition_id : str
27
+ ID of the ensemble definition to delete.
28
+
29
+ Raises
30
+ ------
31
+ requests.HTTPError
32
+ If the response status code is not 2xx.
33
+
34
+ Examples
35
+ --------
36
+ >>> app.delete_ensemble_definition("development-ensemble-definition")
37
+ """
38
+
39
+ _ = self.client.request(
40
+ method="DELETE",
41
+ endpoint=f"{self.ensembles_endpoint}/{ensemble_definition_id}",
42
+ )
43
+
44
+ def ensemble_definition(self: "Application", ensemble_definition_id: str) -> EnsembleDefinition:
45
+ """
46
+ Get an ensemble definition.
47
+
48
+ Parameters
49
+ ----------
50
+ ensemble_definition_id : str
51
+ ID of the ensemble definition to retrieve.
52
+
53
+ Returns
54
+ -------
55
+ EnsembleDefintion
56
+ The requested ensemble definition details.
57
+
58
+ Raises
59
+ ------
60
+ requests.HTTPError
61
+ If the response status code is not 2xx.
62
+
63
+ Examples
64
+ --------
65
+ >>> ensemble_definition = app.ensemble_definition("instance-123")
66
+ >>> print(ensemble_definition.name)
67
+ 'Production Ensemble Definition'
68
+ """
69
+
70
+ response = self.client.request(
71
+ method="GET",
72
+ endpoint=f"{self.ensembles_endpoint}/{ensemble_definition_id}",
73
+ )
74
+
75
+ return EnsembleDefinition.from_dict(response.json())
76
+
77
+ def list_ensemble_definitions(self: "Application") -> list[EnsembleDefinition]:
78
+ """
79
+ List all ensemble_definitions.
80
+
81
+ Returns
82
+ -------
83
+ list[EnsembleDefinition]
84
+ List of all ensemble definitions associated with this application.
85
+
86
+ Raises
87
+ ------
88
+ requests.HTTPError
89
+ If the response status code is not 2xx.
90
+
91
+ Examples
92
+ --------
93
+ >>> ensemble_definitions = app.list_ensemble_definitions()
94
+ >>> for ensemble_definition in ensemble_definitions:
95
+ ... print(ensemble_definition.name)
96
+ 'Development Ensemble Definition'
97
+ 'Production Ensemble Definition'
98
+ """
99
+
100
+ response = self.client.request(
101
+ method="GET",
102
+ endpoint=f"{self.ensembles_endpoint}",
103
+ )
104
+
105
+ return [EnsembleDefinition.from_dict(ensemble_definition) for ensemble_definition in response.json()["items"]]
106
+
107
+ def new_ensemble_defintion(
108
+ self: "Application",
109
+ id: str,
110
+ run_groups: list[RunGroup],
111
+ rules: list[EvaluationRule],
112
+ name: str | None = None,
113
+ description: str | None = None,
114
+ ) -> EnsembleDefinition:
115
+ """
116
+ !!! warning
117
+ `new_ensemble_defintion` is deprecated, use `new_ensemble_definition` instead.
118
+
119
+ Create a new ensemble definition.
120
+
121
+ Parameters
122
+ ----------
123
+ id: str
124
+ ID of the ensemble defintion.
125
+ run_groups: list[RunGroup]
126
+ Information to facilitate the execution of child runs.
127
+ rules: list[EvaluationRule]
128
+ Information to facilitate the selection of
129
+ a result for the ensemble run from child runs.
130
+ name: Optional[str]
131
+ Name of the ensemble definition.
132
+ description: Optional[str]
133
+ Description of the ensemble definition.
134
+ """
135
+
136
+ deprecated(
137
+ name="new_ensemble_defintion",
138
+ reason="`Application.new_ensemble_defintion` is deprecated, use `new_ensemble_definition` instead",
139
+ )
140
+
141
+ return self.new_ensemble_definition(
142
+ run_groups=run_groups,
143
+ rules=rules,
144
+ id=id,
145
+ name=name,
146
+ description=description,
147
+ )
148
+
149
+ def new_ensemble_definition(
150
+ self: "Application",
151
+ run_groups: list[RunGroup],
152
+ rules: list[EvaluationRule],
153
+ id: str | None = None,
154
+ name: str | None = None,
155
+ description: str | None = None,
156
+ ) -> EnsembleDefinition:
157
+ """
158
+ Create a new ensemble definition.
159
+
160
+ Parameters
161
+ ----------
162
+ run_groups: list[RunGroup]
163
+ Information to facilitate the execution of child runs.
164
+ rules: list[EvaluationRule]
165
+ Information to facilitate the selection of
166
+ a result for the ensemble run from child runs.
167
+ id: str | None, default=None
168
+ ID of the ensemble definition. If not provided, a unique ID will be
169
+ generated with the prefix 'ensemble-'.
170
+ name: Optional[str]
171
+ Name of the ensemble definition. If not provided, the ID will be used.
172
+ description: Optional[str]
173
+ Description of the ensemble definition. If not provided, the name will be used.
174
+ """
175
+
176
+ if len(run_groups) == 0:
177
+ raise ValueError("at least one run group must be defined to create an ensemble definition")
178
+
179
+ if len(rules) == 0:
180
+ raise ValueError("at least one evaluation rule must be defined to create an ensemble definition")
181
+
182
+ if id is None or id == "":
183
+ id = safe_id(prefix="ensemble")
184
+ if name is None or name == "":
185
+ name = id
186
+ if description is None or description == "":
187
+ description = name
188
+
189
+ payload = {
190
+ "id": id,
191
+ "run_groups": [run_group.to_dict() for run_group in run_groups],
192
+ "rules": [rule.to_dict() for rule in rules],
193
+ "name": name,
194
+ "description": description,
195
+ }
196
+
197
+ response = self.client.request(
198
+ method="POST",
199
+ endpoint=f"{self.ensembles_endpoint}",
200
+ payload=payload,
201
+ )
202
+
203
+ return EnsembleDefinition.from_dict(response.json())
204
+
205
+ def update_ensemble_definition(
206
+ self: "Application",
207
+ id: str,
208
+ name: str | None = None,
209
+ description: str | None = None,
210
+ ) -> EnsembleDefinition:
211
+ """
212
+ Update an ensemble definition.
213
+
214
+ Parameters
215
+ ----------
216
+ id : str
217
+ ID of the ensemble definition to update.
218
+ name : Optional[str], default=None
219
+ Optional name of the ensemble definition.
220
+ description : Optional[str], default=None
221
+ Optional description of the ensemble definition.
222
+
223
+ Returns
224
+ -------
225
+ EnsembleDefinition
226
+ The updated ensemble definition.
227
+
228
+ Raises
229
+ ------
230
+ ValueError
231
+ If neither name nor description is updated
232
+ requests.HTTPError
233
+ If the response status code is not 2xx.
234
+ """
235
+
236
+ payload = {}
237
+
238
+ if name is None and description is None:
239
+ raise ValueError("Must define at least one value among name and description to modify")
240
+ if name is not None:
241
+ payload["name"] = name
242
+ if description is not None:
243
+ payload["description"] = description
244
+
245
+ response = self.client.request(
246
+ method="PATCH",
247
+ endpoint=f"{self.ensembles_endpoint}/{id}",
248
+ payload=payload,
249
+ )
250
+
251
+ return EnsembleDefinition.from_dict(response.json())
@@ -0,0 +1,263 @@
1
+ """
2
+ Application mixin for managing app input sets.
3
+ """
4
+
5
+ from datetime import datetime
6
+ from typing import TYPE_CHECKING
7
+
8
+ from nextmv.cloud.input_set import InputSet, ManagedInput
9
+ from nextmv.safe import safe_id
10
+
11
+ if TYPE_CHECKING:
12
+ from . import Application
13
+
14
+
15
+ class ApplicationInputSetMixin:
16
+ """
17
+ Mixin class for managing app input sets within an application.
18
+ """
19
+
20
+ def delete_input_set(self: "Application", input_set_id: str) -> None:
21
+ """
22
+ Delete an input set.
23
+
24
+ Deletes an input set along with all the associated information.
25
+
26
+ Parameters
27
+ ----------
28
+ input_set_id : str
29
+ ID of the input set to delete.
30
+
31
+ Raises
32
+ ------
33
+ requests.HTTPError
34
+ If the response status code is not 2xx.
35
+
36
+ Examples
37
+ --------
38
+ >>> app.delete_input_set("input-set-123")
39
+ """
40
+
41
+ _ = self.client.request(
42
+ method="DELETE",
43
+ endpoint=f"{self.experiments_endpoint}/inputsets/{input_set_id}",
44
+ )
45
+
46
+ def input_set(self: "Application", input_set_id: str) -> InputSet:
47
+ """
48
+ Get an input set.
49
+
50
+ Parameters
51
+ ----------
52
+ input_set_id : str
53
+ ID of the input set to retrieve.
54
+
55
+ Returns
56
+ -------
57
+ InputSet
58
+ The requested input set.
59
+
60
+ Raises
61
+ ------
62
+ requests.HTTPError
63
+ If the response status code is not 2xx.
64
+
65
+ Examples
66
+ --------
67
+ >>> input_set = app.input_set("input-set-123")
68
+ >>> print(input_set.name)
69
+ 'My Input Set'
70
+ """
71
+
72
+ response = self.client.request(
73
+ method="GET",
74
+ endpoint=f"{self.experiments_endpoint}/inputsets/{input_set_id}",
75
+ )
76
+
77
+ return InputSet.from_dict(response.json())
78
+
79
+ def list_input_sets(self: "Application") -> list[InputSet]:
80
+ """
81
+ List all input sets.
82
+
83
+ Returns
84
+ -------
85
+ list[InputSet]
86
+ List of all input sets associated with this application.
87
+
88
+ Raises
89
+ ------
90
+ requests.HTTPError
91
+ If the response status code is not 2xx.
92
+
93
+ Examples
94
+ --------
95
+ >>> input_sets = app.list_input_sets()
96
+ >>> for input_set in input_sets:
97
+ ... print(input_set.name)
98
+ 'Input Set 1'
99
+ 'Input Set 2'
100
+ """
101
+
102
+ response = self.client.request(
103
+ method="GET",
104
+ endpoint=f"{self.experiments_endpoint}/inputsets",
105
+ )
106
+
107
+ return [InputSet.from_dict(input_set) for input_set in response.json()]
108
+
109
+ def new_input_set(
110
+ self: "Application",
111
+ id: str | None = None,
112
+ name: str | None = None,
113
+ description: str | None = None,
114
+ end_time: datetime | None = None,
115
+ instance_id: str | None = None,
116
+ maximum_runs: int | None = None,
117
+ run_ids: list[str] | None = None,
118
+ start_time: datetime | None = None,
119
+ inputs: list[ManagedInput] | None = None,
120
+ ) -> InputSet:
121
+ """
122
+ Create a new input set. You can create an input set from three
123
+ different methodologies:
124
+
125
+ 1. Using `instance_id`, `start_time`, `end_time` and `maximum_runs`.
126
+ Instance runs will be obtained from the application matching the
127
+ criteria of dates and maximum number of runs.
128
+ 2. Using `run_ids`. The input set will be created using the list of
129
+ runs specified by the user.
130
+ 3. Using `inputs`. The input set will be created using the list of
131
+ inputs specified by the user. This is useful for creating an input
132
+ set from a list of inputs that are already available in the
133
+ application.
134
+
135
+ Parameters
136
+ ----------
137
+ id: str | None = None
138
+ ID of the input set, will be generated if not provided.
139
+ name: str | None = None
140
+ Name of the input set. If not provided, the ID will be used as
141
+ the name.
142
+ description: Optional[str]
143
+ Optional description of the input set.
144
+ end_time: Optional[datetime]
145
+ End time of the input set. This is used to filter the runs
146
+ associated with the input set.
147
+ instance_id: Optional[str]
148
+ ID of the instance to use for the input set. This is used to
149
+ filter the runs associated with the input set. If not provided,
150
+ the application's `default_instance_id` is used.
151
+ maximum_runs: Optional[int]
152
+ Maximum number of runs to use for the input set. This is used to
153
+ filter the runs associated with the input set. If not provided,
154
+ all runs are used.
155
+ run_ids: Optional[list[str]]
156
+ List of run IDs to use for the input set.
157
+ start_time: Optional[datetime]
158
+ Start time of the input set. This is used to filter the runs
159
+ associated with the input set.
160
+ inputs: Optional[list[ManagedInput]]
161
+ List of inputs to use for the input set. This is used to create
162
+ the input set from a list of inputs that are already available in
163
+ the application.
164
+
165
+ Returns
166
+ -------
167
+ InputSet
168
+ The new input set.
169
+
170
+ Raises
171
+ ------
172
+ requests.HTTPError
173
+ If the response status code is not 2xx.
174
+ """
175
+
176
+ # Generate ID if not provided
177
+ if id is None or id == "":
178
+ id = safe_id("input-set")
179
+
180
+ # Use ID as name if name not provided
181
+ if name is None or name == "":
182
+ name = id
183
+
184
+ payload = {
185
+ "id": id,
186
+ "name": name,
187
+ }
188
+ if description is not None:
189
+ payload["description"] = description
190
+ if end_time is not None:
191
+ payload["end_time"] = end_time.isoformat()
192
+ if instance_id is not None:
193
+ payload["instance_id"] = instance_id
194
+ if maximum_runs is not None:
195
+ payload["maximum_runs"] = maximum_runs
196
+ if run_ids is not None:
197
+ payload["run_ids"] = run_ids
198
+ if start_time is not None:
199
+ payload["start_time"] = start_time.isoformat()
200
+ if inputs is not None:
201
+ payload["inputs"] = [input.to_dict() for input in inputs]
202
+
203
+ response = self.client.request(
204
+ method="POST",
205
+ endpoint=f"{self.experiments_endpoint}/inputsets",
206
+ payload=payload,
207
+ )
208
+
209
+ return InputSet.from_dict(response.json())
210
+
211
+ def update_input_set(
212
+ self: "Application",
213
+ id: str,
214
+ name: str | None = None,
215
+ description: str | None = None,
216
+ inputs: list[ManagedInput] | None = None,
217
+ ) -> InputSet:
218
+ """
219
+ Update an input set.
220
+
221
+ Parameters
222
+ ----------
223
+ id : str
224
+ ID of the input set to update.
225
+ name : Optional[str], default=None
226
+ Optional name of the input set.
227
+ description : Optional[str], default=None
228
+ Optional description of the input set.
229
+ inputs: Optional[list[ManagedInput]]
230
+ List of inputs to use for the input set. This is used to create
231
+ the input set from a list of inputs that are already available in
232
+ the application.
233
+
234
+ Returns
235
+ -------
236
+ Instance
237
+ The updated instance.
238
+
239
+ Raises
240
+ ------
241
+ requests.HTTPError
242
+ If the response status code is not 2xx.
243
+ """
244
+
245
+ # Get the input set as it currently exsits.
246
+ input_set = self.input_set(id)
247
+ input_set_dict = input_set.to_dict()
248
+ payload = input_set_dict.copy()
249
+
250
+ if name is not None:
251
+ payload["name"] = name
252
+ if description is not None:
253
+ payload["description"] = description
254
+ if inputs is not None:
255
+ payload["inputs"] = [input.to_dict() for input in inputs]
256
+
257
+ response = self.client.request(
258
+ method="PUT",
259
+ endpoint=f"{self.experiments_endpoint}/inputsets/{id}",
260
+ payload=payload,
261
+ )
262
+
263
+ return InputSet.from_dict(response.json())