nextmv 0.18.0__py3-none-any.whl → 1.0.0.dev2__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 (175) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__entrypoint__.py +8 -13
  3. nextmv/__init__.py +53 -0
  4. nextmv/_serialization.py +96 -0
  5. nextmv/base_model.py +54 -9
  6. nextmv/cli/CONTRIBUTING.md +511 -0
  7. nextmv/cli/__init__.py +0 -0
  8. nextmv/cli/cloud/__init__.py +47 -0
  9. nextmv/cli/cloud/acceptance/__init__.py +27 -0
  10. nextmv/cli/cloud/acceptance/create.py +393 -0
  11. nextmv/cli/cloud/acceptance/delete.py +68 -0
  12. nextmv/cli/cloud/acceptance/get.py +104 -0
  13. nextmv/cli/cloud/acceptance/list.py +62 -0
  14. nextmv/cli/cloud/acceptance/update.py +95 -0
  15. nextmv/cli/cloud/account/__init__.py +28 -0
  16. nextmv/cli/cloud/account/create.py +83 -0
  17. nextmv/cli/cloud/account/delete.py +60 -0
  18. nextmv/cli/cloud/account/get.py +66 -0
  19. nextmv/cli/cloud/account/update.py +70 -0
  20. nextmv/cli/cloud/app/__init__.py +35 -0
  21. nextmv/cli/cloud/app/create.py +141 -0
  22. nextmv/cli/cloud/app/delete.py +58 -0
  23. nextmv/cli/cloud/app/exists.py +44 -0
  24. nextmv/cli/cloud/app/get.py +66 -0
  25. nextmv/cli/cloud/app/list.py +61 -0
  26. nextmv/cli/cloud/app/push.py +137 -0
  27. nextmv/cli/cloud/app/update.py +124 -0
  28. nextmv/cli/cloud/batch/__init__.py +29 -0
  29. nextmv/cli/cloud/batch/create.py +454 -0
  30. nextmv/cli/cloud/batch/delete.py +68 -0
  31. nextmv/cli/cloud/batch/get.py +104 -0
  32. nextmv/cli/cloud/batch/list.py +63 -0
  33. nextmv/cli/cloud/batch/metadata.py +66 -0
  34. nextmv/cli/cloud/batch/update.py +95 -0
  35. nextmv/cli/cloud/data/__init__.py +26 -0
  36. nextmv/cli/cloud/data/upload.py +162 -0
  37. nextmv/cli/cloud/ensemble/__init__.py +31 -0
  38. nextmv/cli/cloud/ensemble/create.py +414 -0
  39. nextmv/cli/cloud/ensemble/delete.py +67 -0
  40. nextmv/cli/cloud/ensemble/get.py +65 -0
  41. nextmv/cli/cloud/ensemble/update.py +103 -0
  42. nextmv/cli/cloud/input_set/__init__.py +30 -0
  43. nextmv/cli/cloud/input_set/create.py +170 -0
  44. nextmv/cli/cloud/input_set/get.py +63 -0
  45. nextmv/cli/cloud/input_set/list.py +63 -0
  46. nextmv/cli/cloud/input_set/update.py +123 -0
  47. nextmv/cli/cloud/instance/__init__.py +35 -0
  48. nextmv/cli/cloud/instance/create.py +290 -0
  49. nextmv/cli/cloud/instance/delete.py +62 -0
  50. nextmv/cli/cloud/instance/exists.py +39 -0
  51. nextmv/cli/cloud/instance/get.py +62 -0
  52. nextmv/cli/cloud/instance/list.py +60 -0
  53. nextmv/cli/cloud/instance/update.py +216 -0
  54. nextmv/cli/cloud/managed_input/__init__.py +31 -0
  55. nextmv/cli/cloud/managed_input/create.py +146 -0
  56. nextmv/cli/cloud/managed_input/delete.py +65 -0
  57. nextmv/cli/cloud/managed_input/get.py +63 -0
  58. nextmv/cli/cloud/managed_input/list.py +60 -0
  59. nextmv/cli/cloud/managed_input/update.py +97 -0
  60. nextmv/cli/cloud/run/__init__.py +37 -0
  61. nextmv/cli/cloud/run/cancel.py +37 -0
  62. nextmv/cli/cloud/run/create.py +530 -0
  63. nextmv/cli/cloud/run/get.py +199 -0
  64. nextmv/cli/cloud/run/input.py +86 -0
  65. nextmv/cli/cloud/run/list.py +80 -0
  66. nextmv/cli/cloud/run/logs.py +167 -0
  67. nextmv/cli/cloud/run/metadata.py +67 -0
  68. nextmv/cli/cloud/run/track.py +501 -0
  69. nextmv/cli/cloud/scenario/__init__.py +29 -0
  70. nextmv/cli/cloud/scenario/create.py +451 -0
  71. nextmv/cli/cloud/scenario/delete.py +65 -0
  72. nextmv/cli/cloud/scenario/get.py +102 -0
  73. nextmv/cli/cloud/scenario/list.py +63 -0
  74. nextmv/cli/cloud/scenario/metadata.py +67 -0
  75. nextmv/cli/cloud/scenario/update.py +93 -0
  76. nextmv/cli/cloud/secrets/__init__.py +33 -0
  77. nextmv/cli/cloud/secrets/create.py +206 -0
  78. nextmv/cli/cloud/secrets/delete.py +67 -0
  79. nextmv/cli/cloud/secrets/get.py +66 -0
  80. nextmv/cli/cloud/secrets/list.py +60 -0
  81. nextmv/cli/cloud/secrets/update.py +147 -0
  82. nextmv/cli/cloud/shadow/__init__.py +33 -0
  83. nextmv/cli/cloud/shadow/create.py +184 -0
  84. nextmv/cli/cloud/shadow/delete.py +68 -0
  85. nextmv/cli/cloud/shadow/get.py +61 -0
  86. nextmv/cli/cloud/shadow/list.py +63 -0
  87. nextmv/cli/cloud/shadow/metadata.py +66 -0
  88. nextmv/cli/cloud/shadow/start.py +43 -0
  89. nextmv/cli/cloud/shadow/stop.py +43 -0
  90. nextmv/cli/cloud/shadow/update.py +95 -0
  91. nextmv/cli/cloud/upload/__init__.py +22 -0
  92. nextmv/cli/cloud/upload/create.py +39 -0
  93. nextmv/cli/cloud/version/__init__.py +33 -0
  94. nextmv/cli/cloud/version/create.py +97 -0
  95. nextmv/cli/cloud/version/delete.py +62 -0
  96. nextmv/cli/cloud/version/exists.py +39 -0
  97. nextmv/cli/cloud/version/get.py +62 -0
  98. nextmv/cli/cloud/version/list.py +60 -0
  99. nextmv/cli/cloud/version/update.py +92 -0
  100. nextmv/cli/community/__init__.py +24 -0
  101. nextmv/cli/community/clone.py +270 -0
  102. nextmv/cli/community/list.py +265 -0
  103. nextmv/cli/configuration/__init__.py +23 -0
  104. nextmv/cli/configuration/config.py +195 -0
  105. nextmv/cli/configuration/create.py +94 -0
  106. nextmv/cli/configuration/delete.py +67 -0
  107. nextmv/cli/configuration/list.py +77 -0
  108. nextmv/cli/main.py +188 -0
  109. nextmv/cli/message.py +153 -0
  110. nextmv/cli/options.py +206 -0
  111. nextmv/cli/version.py +38 -0
  112. nextmv/cloud/__init__.py +71 -17
  113. nextmv/cloud/acceptance_test.py +757 -51
  114. nextmv/cloud/account.py +406 -17
  115. nextmv/cloud/application/__init__.py +957 -0
  116. nextmv/cloud/application/_acceptance.py +419 -0
  117. nextmv/cloud/application/_batch_scenario.py +860 -0
  118. nextmv/cloud/application/_ensemble.py +251 -0
  119. nextmv/cloud/application/_input_set.py +227 -0
  120. nextmv/cloud/application/_instance.py +289 -0
  121. nextmv/cloud/application/_managed_input.py +227 -0
  122. nextmv/cloud/application/_run.py +1393 -0
  123. nextmv/cloud/application/_secrets.py +294 -0
  124. nextmv/cloud/application/_shadow.py +314 -0
  125. nextmv/cloud/application/_utils.py +54 -0
  126. nextmv/cloud/application/_version.py +303 -0
  127. nextmv/cloud/assets.py +48 -0
  128. nextmv/cloud/batch_experiment.py +294 -33
  129. nextmv/cloud/client.py +307 -66
  130. nextmv/cloud/ensemble.py +247 -0
  131. nextmv/cloud/input_set.py +120 -2
  132. nextmv/cloud/instance.py +133 -8
  133. nextmv/cloud/integration.py +533 -0
  134. nextmv/cloud/package.py +168 -53
  135. nextmv/cloud/scenario.py +410 -0
  136. nextmv/cloud/secrets.py +234 -0
  137. nextmv/cloud/shadow.py +190 -0
  138. nextmv/cloud/url.py +73 -0
  139. nextmv/cloud/version.py +132 -4
  140. nextmv/default_app/.gitignore +1 -0
  141. nextmv/default_app/README.md +32 -0
  142. nextmv/default_app/app.yaml +12 -0
  143. nextmv/default_app/input.json +5 -0
  144. nextmv/default_app/main.py +37 -0
  145. nextmv/default_app/requirements.txt +2 -0
  146. nextmv/default_app/src/__init__.py +0 -0
  147. nextmv/default_app/src/visuals.py +36 -0
  148. nextmv/deprecated.py +47 -0
  149. nextmv/input.py +861 -90
  150. nextmv/local/__init__.py +5 -0
  151. nextmv/local/application.py +1251 -0
  152. nextmv/local/executor.py +1042 -0
  153. nextmv/local/geojson_handler.py +323 -0
  154. nextmv/local/local.py +97 -0
  155. nextmv/local/plotly_handler.py +61 -0
  156. nextmv/local/runner.py +274 -0
  157. nextmv/logger.py +80 -9
  158. nextmv/manifest.py +1466 -0
  159. nextmv/model.py +241 -66
  160. nextmv/options.py +708 -115
  161. nextmv/output.py +1301 -274
  162. nextmv/polling.py +325 -0
  163. nextmv/run.py +1702 -0
  164. nextmv/safe.py +145 -0
  165. nextmv/status.py +122 -0
  166. nextmv-1.0.0.dev2.dist-info/METADATA +311 -0
  167. nextmv-1.0.0.dev2.dist-info/RECORD +170 -0
  168. {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/WHEEL +1 -1
  169. nextmv-1.0.0.dev2.dist-info/entry_points.txt +2 -0
  170. nextmv/cloud/application.py +0 -1405
  171. nextmv/cloud/manifest.py +0 -234
  172. nextmv/cloud/status.py +0 -29
  173. nextmv-0.18.0.dist-info/METADATA +0 -770
  174. nextmv-0.18.0.dist-info/RECORD +0 -25
  175. {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,860 @@
1
+ """
2
+ Application mixin for managing batch experiments.
3
+ """
4
+
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from nextmv.cloud.batch_experiment import (
8
+ BatchExperiment,
9
+ BatchExperimentInformation,
10
+ BatchExperimentMetadata,
11
+ BatchExperimentRun,
12
+ ExperimentStatus,
13
+ to_runs,
14
+ )
15
+ from nextmv.cloud.input_set import InputSet, ManagedInput
16
+ from nextmv.cloud.scenario import Scenario, ScenarioInputType, _option_sets, _scenarios_by_id
17
+ from nextmv.polling import DEFAULT_POLLING_OPTIONS, PollingOptions, poll
18
+ from nextmv.run import Run
19
+ from nextmv.safe import safe_id, safe_name_and_id
20
+
21
+ if TYPE_CHECKING:
22
+ from . import Application
23
+
24
+
25
+ class ApplicationBatchMixin:
26
+ """
27
+ Mixin class for managing batch experiments within an application.
28
+ """
29
+
30
+ def batch_experiment(self: "Application", batch_id: str) -> BatchExperiment:
31
+ """
32
+ Get a batch experiment. This method also returns the runs of the batch
33
+ experiment under the `.runs` attribute.
34
+
35
+ Parameters
36
+ ----------
37
+ batch_id : str
38
+ ID of the batch experiment.
39
+
40
+ Returns
41
+ -------
42
+ BatchExperiment
43
+ The requested batch experiment details.
44
+
45
+ Raises
46
+ ------
47
+ requests.HTTPError
48
+ If the response status code is not 2xx.
49
+
50
+ Examples
51
+ --------
52
+ >>> batch_exp = app.batch_experiment("batch-123")
53
+ >>> print(batch_exp.name)
54
+ 'My Batch Experiment'
55
+ """
56
+
57
+ response = self.client.request(
58
+ method="GET",
59
+ endpoint=f"{self.experiments_endpoint}/batch/{batch_id}",
60
+ )
61
+
62
+ exp = BatchExperiment.from_dict(response.json())
63
+
64
+ runs_response = self.client.request(
65
+ method="GET",
66
+ endpoint=f"{self.experiments_endpoint}/batch/{batch_id}/runs",
67
+ )
68
+
69
+ runs = [Run.from_dict(run) for run in runs_response.json().get("runs", [])]
70
+ exp.runs = runs
71
+
72
+ return exp
73
+
74
+ def batch_experiment_metadata(self: "Application", batch_id: str) -> BatchExperimentMetadata:
75
+ """
76
+ Get metadata for a batch experiment.
77
+
78
+ Parameters
79
+ ----------
80
+ batch_id : str
81
+ ID of the batch experiment.
82
+
83
+ Returns
84
+ -------
85
+ BatchExperimentMetadata
86
+ The requested batch experiment metadata.
87
+
88
+ Raises
89
+ ------
90
+ requests.HTTPError
91
+ If the response status code is not 2xx.
92
+
93
+ Examples
94
+ --------
95
+ >>> metadata = app.batch_experiment_metadata("batch-123")
96
+ >>> print(metadata.name)
97
+ 'My Batch Experiment'
98
+ """
99
+
100
+ response = self.client.request(
101
+ method="GET",
102
+ endpoint=f"{self.experiments_endpoint}/batch/{batch_id}/metadata",
103
+ )
104
+
105
+ return BatchExperimentMetadata.from_dict(response.json())
106
+
107
+ def batch_experiment_with_polling(
108
+ self: "Application",
109
+ batch_id: str,
110
+ polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
111
+ ) -> BatchExperiment:
112
+ """
113
+ Get a batch experiment with polling.
114
+
115
+ Retrieves the result of an experiment. This method polls for the result
116
+ until the experiment finishes executing or the polling strategy is
117
+ exhausted.
118
+
119
+ Parameters
120
+ ----------
121
+ batch_id : str
122
+ ID of the batch experiment.
123
+
124
+ Returns
125
+ -------
126
+ BatchExperiment
127
+ The requested batch experiment details.
128
+
129
+ Raises
130
+ ------
131
+ requests.HTTPError
132
+ If the response status code is not 2xx.
133
+
134
+ Examples
135
+ --------
136
+ >>> batch_exp = app.batch_experiment_with_polling("batch-123")
137
+ >>> print(batch_exp.name)
138
+ 'My Batch Experiment'
139
+ """
140
+
141
+ def polling_func() -> tuple[Any, bool]:
142
+ batch_metadata = self.batch_experiment_metadata(batch_id=batch_id)
143
+ if batch_metadata.status in {
144
+ ExperimentStatus.COMPLETED,
145
+ ExperimentStatus.FAILED,
146
+ ExperimentStatus.DRAFT,
147
+ ExperimentStatus.CANCELED,
148
+ ExperimentStatus.DELETE_FAILED,
149
+ }:
150
+ return batch_metadata, True
151
+
152
+ return None, False
153
+
154
+ batch_information = poll(polling_options=polling_options, polling_func=polling_func)
155
+
156
+ return self.batch_experiment(batch_id=batch_information.id)
157
+
158
+ def delete_batch_experiment(self: "Application", batch_id: str) -> None:
159
+ """
160
+ Delete a batch experiment.
161
+
162
+ Deletes a batch experiment along with all the associated information,
163
+ such as its runs.
164
+
165
+ Parameters
166
+ ----------
167
+ batch_id : str
168
+ ID of the batch experiment to delete.
169
+
170
+ Raises
171
+ ------
172
+ requests.HTTPError
173
+ If the response status code is not 2xx.
174
+
175
+ Examples
176
+ --------
177
+ >>> app.delete_batch_experiment("batch-123")
178
+ """
179
+
180
+ _ = self.client.request(
181
+ method="DELETE",
182
+ endpoint=f"{self.experiments_endpoint}/batch/{batch_id}",
183
+ )
184
+
185
+ def delete_scenario_test(self: "Application", scenario_test_id: str) -> None:
186
+ """
187
+ Delete a scenario test.
188
+
189
+ Deletes a scenario test. Scenario tests are based on the batch
190
+ experiments API, so this function summons `delete_batch_experiment`.
191
+
192
+ Parameters
193
+ ----------
194
+ scenario_test_id : str
195
+ ID of the scenario test to delete.
196
+
197
+ Raises
198
+ ------
199
+ requests.HTTPError
200
+ If the response status code is not 2xx.
201
+
202
+ Examples
203
+ --------
204
+ >>> app.delete_scenario_test("scenario-123")
205
+ """
206
+
207
+ self.delete_batch_experiment(batch_id=scenario_test_id)
208
+
209
+ def list_batch_experiments(self: "Application") -> list[BatchExperimentMetadata]:
210
+ """
211
+ List all batch experiments.
212
+
213
+ Returns
214
+ -------
215
+ list[BatchExperimentMetadata]
216
+ List of batch experiments.
217
+
218
+ Raises
219
+ ------
220
+ requests.HTTPError
221
+ If the response status code is not 2xx.
222
+ """
223
+
224
+ response = self.client.request(
225
+ method="GET",
226
+ endpoint=f"{self.experiments_endpoint}/batch",
227
+ query_params={"type": "batch"},
228
+ )
229
+
230
+ return [BatchExperimentMetadata.from_dict(batch_experiment) for batch_experiment in response.json()]
231
+
232
+ def list_scenario_tests(self: "Application") -> list[BatchExperimentMetadata]:
233
+ """
234
+ List all batch scenario tests. Scenario tests are based on the batch
235
+ experiments API, so this function returns the same information as
236
+ `list_batch_experiments`, albeit using a different query parameter.
237
+
238
+ Returns
239
+ -------
240
+ list[BatchExperimentMetadata]
241
+ List of scenario tests.
242
+
243
+ Raises
244
+ ------
245
+ requests.HTTPError
246
+ If the response status code is not 2xx.
247
+ """
248
+
249
+ response = self.client.request(
250
+ method="GET",
251
+ endpoint=f"{self.experiments_endpoint}/batch",
252
+ query_params={"type": "scenario"},
253
+ )
254
+
255
+ return [BatchExperimentMetadata.from_dict(batch_experiment) for batch_experiment in response.json()]
256
+
257
+ def new_batch_experiment( # noqa: C901
258
+ self: "Application",
259
+ name: str | None = None,
260
+ input_set_id: str | None = None,
261
+ instance_ids: list[str] | None = None,
262
+ description: str | None = None,
263
+ id: str | None = None,
264
+ option_sets: dict[str, dict[str, str]] | None = None,
265
+ runs: list[BatchExperimentRun | dict[str, Any]] | None = None,
266
+ type: str | None = "batch",
267
+ ) -> str:
268
+ """
269
+ Create a new batch experiment.
270
+
271
+ Parameters
272
+ ----------
273
+ name: Optional[str]
274
+ Name of the batch experiment. If not provided, the ID will be used as the name.
275
+ input_set_id: str | None
276
+ ID of the input set to use for the batch experiment.
277
+ instance_ids: list[str]
278
+ This argument is deprecated, use `runs` instead.
279
+ List of instance IDs to use for the batch experiment.
280
+ description: Optional[str]
281
+ Optional description of the batch experiment.
282
+ id: Optional[str]
283
+ ID of the batch experiment. Will be generated if not provided.
284
+ option_sets: Optional[dict[str, dict[str, str]]]
285
+ Option sets to use for the batch experiment. This is a dictionary
286
+ where the keys are option set IDs and the values are dictionaries
287
+ with the actual options.
288
+ runs: Optional[list[BatchExperimentRun]]
289
+ List of runs to use for the batch experiment.
290
+ type: Optional[str]
291
+ Type of the batch experiment. This is used to determine the
292
+ experiment type. The default value is "batch". If you want to
293
+ create a scenario test, set this to "scenario".
294
+
295
+ Returns
296
+ -------
297
+ str
298
+ ID of the batch experiment.
299
+
300
+ Raises
301
+ ------
302
+ requests.HTTPError
303
+ If the response status code is not 2xx.
304
+ """
305
+
306
+ # Generate ID if not provided
307
+ if id is None:
308
+ id = safe_id("batch")
309
+
310
+ # Use ID as name if name not provided
311
+ if name is None:
312
+ name = id
313
+
314
+ payload = {
315
+ "id": id,
316
+ "name": name,
317
+ }
318
+ if input_set_id is not None:
319
+ payload["input_set_id"] = input_set_id
320
+ if instance_ids is not None:
321
+ input_set = self.input_set(input_set_id)
322
+ runs = to_runs(instance_ids, input_set)
323
+ payload_runs = [run.to_dict() for run in runs]
324
+ payload["runs"] = payload_runs
325
+ if description is not None:
326
+ payload["description"] = description
327
+ if option_sets is not None:
328
+ payload["option_sets"] = option_sets
329
+ if runs is not None:
330
+ payload_runs = [{}] * len(runs)
331
+ for i, run in enumerate(runs):
332
+ payload_runs[i] = run.to_dict() if isinstance(run, BatchExperimentRun) else run
333
+ payload["runs"] = payload_runs
334
+ if type is not None:
335
+ payload["type"] = type
336
+
337
+ response = self.client.request(
338
+ method="POST",
339
+ endpoint=f"{self.experiments_endpoint}/batch",
340
+ payload=payload,
341
+ )
342
+
343
+ return response.json()["id"]
344
+
345
+ def new_batch_experiment_with_result(
346
+ self: "Application",
347
+ name: str | None = None,
348
+ input_set_id: str | None = None,
349
+ instance_ids: list[str] | None = None,
350
+ description: str | None = None,
351
+ id: str | None = None,
352
+ option_sets: dict[str, dict[str, str]] | None = None,
353
+ runs: list[BatchExperimentRun | dict[str, Any]] | None = None,
354
+ type: str | None = "batch",
355
+ polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
356
+ ) -> BatchExperiment:
357
+ """
358
+ Convenience method to create a new batch experiment and poll for the
359
+ result.
360
+
361
+ This method combines the `new_batch_experiment` and
362
+ `batch_experiment_with_polling` methods, applying polling logic to
363
+ check when the experiment succeeded.
364
+
365
+ Parameters
366
+ ----------
367
+ name: Optional[str]
368
+ Name of the batch experiment. If not provided, the ID will be used as the name.
369
+ input_set_id: str
370
+ ID of the input set to use for the batch experiment.
371
+ instance_ids: list[str]
372
+ List of instance IDs to use for the batch experiment. This argument
373
+ is deprecated, use `runs` instead.
374
+ description: Optional[str]
375
+ Optional description of the batch experiment.
376
+ id: Optional[str]
377
+ ID of the batch experiment. Will be generated if not provided.
378
+ option_sets: Optional[dict[str, dict[str, str]]]
379
+ Option sets to use for the batch experiment. This is a dictionary
380
+ where the keys are option set IDs and the values are dictionaries
381
+ with the actual options.
382
+ runs: Optional[list[BatchExperimentRun]]
383
+ List of runs to use for the batch experiment.
384
+ type: Optional[str]
385
+ Type of the batch experiment. This is used to determine the
386
+ experiment type. The default value is "batch". If you want to
387
+ create a scenario test, set this to "scenario".
388
+ polling_options : PollingOptions, default=DEFAULT_POLLING_OPTIONS
389
+ Options to use when polling for the batch experiment result.
390
+
391
+ Returns
392
+ -------
393
+ BatchExperiment
394
+ The completed batch experiment with results.
395
+
396
+ Raises
397
+ ------
398
+ requests.HTTPError
399
+ If the response status code is not 2xx.
400
+ """
401
+
402
+ batch_id = self.new_batch_experiment(
403
+ name=name,
404
+ input_set_id=input_set_id,
405
+ instance_ids=instance_ids,
406
+ description=description,
407
+ id=id,
408
+ option_sets=option_sets,
409
+ runs=runs,
410
+ type=type,
411
+ )
412
+
413
+ return self.batch_experiment_with_polling(batch_id=batch_id, polling_options=polling_options)
414
+
415
+ def new_scenario_test(
416
+ self: "Application",
417
+ scenarios: list[Scenario],
418
+ id: str | None = None,
419
+ name: str | None = None,
420
+ description: str | None = None,
421
+ repetitions: int | None = 0,
422
+ ) -> str:
423
+ """
424
+ Create a new scenario test. The test is based on `scenarios` and you
425
+ may specify `repetitions` to run the test multiple times. 0 repetitions
426
+ means that the tests will be executed once. 1 repetition means that the
427
+ test will be repeated once, i.e.: it will be executed twice. 2
428
+ repetitions equals 3 executions, so on, and so forth.
429
+
430
+ For each scenario, consider the `scenario_input` and `configuration`.
431
+ The `scenario_input.scenario_input_type` allows you to specify the data
432
+ that will be used for that scenario.
433
+
434
+ - `ScenarioInputType.INPUT_SET`: the data should be taken from an
435
+ existing input set.
436
+ - `ScenarioInputType.INPUT`: the data should be taken from a list of
437
+ existing inputs. When using this type, an input set will be created
438
+ from this set of managed inputs.
439
+ - `ScenarioInputType.New`: a new set of data will be uploaded as a set
440
+ of managed inputs. A new input set will be created from this set of
441
+ managed inputs.
442
+
443
+ On the other hand, the `configuration` allows you to specify multiple
444
+ option variations for the scenario. Please see the
445
+ `ScenarioConfiguration` class for more information.
446
+
447
+ The scenario tests uses the batch experiments API under the hood.
448
+
449
+ Parameters
450
+ ----------
451
+ scenarios: list[Scenario]
452
+ List of scenarios to use for the scenario test. At least one
453
+ scenario should be provided.
454
+ id: Optional[str]
455
+ ID of the scenario test. Will be generated if not provided.
456
+ name: Optional[str]
457
+ Name of the scenario test. If not provided, the ID will be used as the name.
458
+ description: Optional[str]
459
+ Optional description of the scenario test.
460
+ repetitions: Optional[int]
461
+ Number of repetitions to use for the scenario test. 0
462
+ repetitions means that the tests will be executed once. 1
463
+ repetition means that the test will be repeated once, i.e.: it
464
+ will be executed twice. 2 repetitions equals 3 executions, so on,
465
+ and so forth.
466
+
467
+ Returns
468
+ -------
469
+ str
470
+ ID of the scenario test.
471
+
472
+ Raises
473
+ ------
474
+ requests.HTTPError
475
+ If the response status code is not 2xx.
476
+ ValueError
477
+ If no scenarios are provided.
478
+ """
479
+
480
+ if len(scenarios) < 1:
481
+ raise ValueError("At least one scenario must be provided")
482
+
483
+ # Generate ID if not provided
484
+ if id is None:
485
+ id = safe_id("scenario")
486
+
487
+ # Use ID as name if name not provided
488
+ if name is None:
489
+ name = id
490
+
491
+ scenarios_by_id = _scenarios_by_id(scenarios)
492
+
493
+ # Save all the information needed by scenario.
494
+ input_sets = {}
495
+ instances = {}
496
+ for scenario_id, scenario in scenarios_by_id.items():
497
+ instance = self.instance(instance_id=scenario.instance_id)
498
+
499
+ # Each scenario is associated to an input set, so we must either
500
+ # get it or create it.
501
+ input_set = self.__input_set_for_scenario(scenario, scenario_id)
502
+
503
+ instances[scenario_id] = instance
504
+ input_sets[scenario_id] = input_set
505
+
506
+ # Calculate the combinations of all the option sets across scenarios.
507
+ opt_sets_by_scenario = _option_sets(scenarios)
508
+
509
+ # The scenario tests results in multiple individual runs.
510
+ runs = []
511
+ run_counter = 0
512
+ opt_sets = {}
513
+ for scenario_id, scenario_opt_sets in opt_sets_by_scenario.items():
514
+ opt_sets = {**opt_sets, **scenario_opt_sets}
515
+ input_set = input_sets[scenario_id]
516
+ scenario = scenarios_by_id[scenario_id]
517
+
518
+ for set_key in scenario_opt_sets.keys():
519
+ inputs = input_set.input_ids if len(input_set.input_ids) > 0 else input_set.inputs
520
+ for input in inputs:
521
+ input_id = input.id if isinstance(input, ManagedInput) else input
522
+ for repetition in range(repetitions + 1):
523
+ run_counter += 1
524
+ run = BatchExperimentRun(
525
+ input_id=input_id,
526
+ input_set_id=input_set.id,
527
+ instance_id=scenario.instance_id,
528
+ option_set=set_key,
529
+ scenario_id=scenario_id,
530
+ repetition=repetition,
531
+ run_number=f"{run_counter}",
532
+ )
533
+ runs.append(run)
534
+
535
+ return self.new_batch_experiment(
536
+ id=id,
537
+ name=name,
538
+ description=description,
539
+ type="scenario",
540
+ option_sets=opt_sets,
541
+ runs=runs,
542
+ )
543
+
544
+ def new_scenario_test_with_result(
545
+ self: "Application",
546
+ scenarios: list[Scenario],
547
+ id: str | None = None,
548
+ name: str | None = None,
549
+ description: str | None = None,
550
+ repetitions: int | None = 0,
551
+ polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
552
+ ) -> BatchExperiment:
553
+ """
554
+ Convenience method to create a new scenario test and poll for the
555
+ result.
556
+
557
+ This method combines the `new_scenario_test` and
558
+ `scenario_test_with_polling` methods, applying polling logic to
559
+ check when the test succeeded.
560
+
561
+ The scenario tests uses the batch experiments API under the hood.
562
+
563
+ Parameters
564
+ ----------
565
+ scenarios: list[Scenario]
566
+ List of scenarios to use for the scenario test. At least one
567
+ scenario should be provided.
568
+ id: Optional[str]
569
+ ID of the scenario test. Will be generated if not provided.
570
+ name: Optional[str]
571
+ Name of the scenario test. If not provided, the ID will be used as the name.
572
+ description: Optional[str]
573
+ Optional description of the scenario test.
574
+ repetitions: Optional[int]
575
+ Number of repetitions to use for the scenario test. 0
576
+ repetitions means that the tests will be executed once. 1
577
+ repetition means that the test will be repeated once, i.e.: it
578
+ will be executed twice. 2 repetitions equals 3 executions, so on,
579
+ and so forth.
580
+ polling_options : PollingOptions, default=DEFAULT_POLLING_OPTIONS
581
+ Options to use when polling for the scenario test result.
582
+
583
+ Returns
584
+ -------
585
+ BatchExperiment
586
+ The completed scenario test as a BatchExperiment.
587
+
588
+ Raises
589
+ ------
590
+ requests.HTTPError
591
+ If the response status code is not 2xx.
592
+ ValueError
593
+ If no scenarios are provided.
594
+ """
595
+
596
+ test_id = self.new_scenario_test(
597
+ scenarios=scenarios,
598
+ id=id,
599
+ name=name,
600
+ description=description,
601
+ repetitions=repetitions,
602
+ )
603
+
604
+ return self.scenario_test_with_polling(
605
+ scenario_test_id=test_id,
606
+ polling_options=polling_options,
607
+ )
608
+
609
+ def scenario_test(self: "Application", scenario_test_id: str) -> BatchExperiment:
610
+ """
611
+ Get a scenario test.
612
+
613
+ Retrieves a scenario test by ID. Scenario tests are based on batch
614
+ experiments, so this function returns the corresponding batch
615
+ experiment associated with the scenario test.
616
+
617
+ Parameters
618
+ ----------
619
+ scenario_test_id : str
620
+ ID of the scenario test to retrieve.
621
+
622
+ Returns
623
+ -------
624
+ BatchExperiment
625
+ The scenario test details as a batch experiment.
626
+
627
+ Raises
628
+ ------
629
+ requests.HTTPError
630
+ If the response status code is not 2xx.
631
+
632
+ Examples
633
+ --------
634
+ >>> test = app.scenario_test("scenario-123")
635
+ >>> print(test.name)
636
+ 'My Scenario Test'
637
+ >>> print(test.type)
638
+ 'scenario'
639
+ """
640
+
641
+ return self.batch_experiment(batch_id=scenario_test_id)
642
+
643
+ def scenario_test_metadata(self: "Application", scenario_test_id: str) -> BatchExperimentMetadata:
644
+ """
645
+ Get the metadata for a scenario test, given its ID.
646
+
647
+ Scenario tests are based on batch experiments, so this function returns
648
+ the corresponding batch experiment metadata associated with the
649
+ scenario test.
650
+
651
+ Parameters
652
+ ----------
653
+ scenario_test_id : str
654
+ ID of the scenario test to retrieve.
655
+
656
+ Returns
657
+ -------
658
+ BatchExperimentMetadata
659
+ The scenario test metadata as a batch experiment.
660
+
661
+ Raises
662
+ ------
663
+ requests.HTTPError
664
+ If the response status code is not 2xx.
665
+
666
+ Examples
667
+ --------
668
+ >>> metadata = app.scenario_test_metadata("scenario-123")
669
+ >>> print(metadata.name)
670
+ 'My Scenario Test'
671
+ >>> print(metadata.type)
672
+ 'scenario'
673
+ """
674
+
675
+ return self.batch_experiment_metadata(batch_id=scenario_test_id)
676
+
677
+ def scenario_test_with_polling(
678
+ self: "Application",
679
+ scenario_test_id: str,
680
+ polling_options: PollingOptions = DEFAULT_POLLING_OPTIONS,
681
+ ) -> BatchExperiment:
682
+ """
683
+ Get a scenario test with polling.
684
+
685
+ Retrieves the result of a scenario test. This method polls for the
686
+ result until the test finishes executing or the polling strategy is
687
+ exhausted.
688
+
689
+ The scenario tests uses the batch experiments API under the hood.
690
+
691
+ Parameters
692
+ ----------
693
+ scenario_test_id : str
694
+ ID of the scenario test to retrieve.
695
+ polling_options : PollingOptions, default=DEFAULT_POLLING_OPTIONS
696
+ Options to use when polling for the scenario test result.
697
+
698
+ Returns
699
+ -------
700
+ BatchExperiment
701
+ The scenario test details as a batch experiment.
702
+
703
+ Raises
704
+ ------
705
+ requests.HTTPError
706
+ If the response status code is not 2xx.
707
+
708
+ Examples
709
+ --------
710
+ >>> test = app.scenario_test_with_polling("scenario-123")
711
+ >>> print(test.name)
712
+ 'My Scenario Test'
713
+ >>> print(test.type)
714
+ 'scenario'
715
+ """
716
+
717
+ return self.batch_experiment_with_polling(batch_id=scenario_test_id, polling_options=polling_options)
718
+
719
+ def update_batch_experiment(
720
+ self: "Application",
721
+ batch_experiment_id: str,
722
+ name: str | None = None,
723
+ description: str | None = None,
724
+ ) -> BatchExperimentInformation:
725
+ """
726
+ Update a batch experiment.
727
+
728
+ Parameters
729
+ ----------
730
+ batch_experiment_id : str
731
+ ID of the batch experiment to update.
732
+ name : Optional[str], default=None
733
+ Optional name of the batch experiment.
734
+ description : Optional[str], default=None
735
+ Optional description of the batch experiment.
736
+
737
+ Returns
738
+ -------
739
+ BatchExperimentInformation
740
+ The information with the updated batch experiment.
741
+
742
+ Raises
743
+ ------
744
+ requests.HTTPError
745
+ If the response status code is not 2xx.
746
+ """
747
+
748
+ payload = {}
749
+
750
+ if name is not None:
751
+ payload["name"] = name
752
+ if description is not None:
753
+ payload["description"] = description
754
+
755
+ response = self.client.request(
756
+ method="PATCH",
757
+ endpoint=f"{self.experiments_endpoint}/batch/{batch_experiment_id}",
758
+ payload=payload,
759
+ )
760
+
761
+ return BatchExperimentInformation.from_dict(response.json())
762
+
763
+ def update_scenario_test(
764
+ self: "Application",
765
+ scenario_test_id: str,
766
+ name: str | None = None,
767
+ description: str | None = None,
768
+ ) -> BatchExperimentInformation:
769
+ """
770
+ Update a scenario test.
771
+
772
+ Updates a scenario test with new name and description. Scenario tests
773
+ use the batch experiments API, so this method calls the
774
+ `update_batch_experiment` method, and thus the return type is the same.
775
+
776
+ Parameters
777
+ ----------
778
+ scenario_test_id : str
779
+ ID of the scenario test to update.
780
+ name : Optional[str], default=None
781
+ Optional new name for the scenario test.
782
+ description : Optional[str], default=None
783
+ Optional new description for the scenario test.
784
+
785
+ Returns
786
+ -------
787
+ BatchExperimentInformation
788
+ The information about the updated scenario test.
789
+
790
+ Raises
791
+ ------
792
+ requests.HTTPError
793
+ If the response status code is not 2xx.
794
+
795
+ Examples
796
+ --------
797
+ >>> info = app.update_scenario_test(
798
+ ... scenario_test_id="scenario-123",
799
+ ... name="Updated Test Name",
800
+ ... description="Updated description for this test"
801
+ ... )
802
+ >>> print(info.name)
803
+ 'Updated Test Name'
804
+ """
805
+
806
+ return self.update_batch_experiment(
807
+ batch_experiment_id=scenario_test_id,
808
+ name=name,
809
+ description=description,
810
+ )
811
+
812
+ def __input_set_for_scenario(self: "Application", scenario: Scenario, scenario_id: str) -> InputSet:
813
+ # If working with an input set, there is no need to create one.
814
+ if scenario.scenario_input.scenario_input_type == ScenarioInputType.INPUT_SET:
815
+ input_set = self.input_set(input_set_id=scenario.scenario_input.scenario_input_data)
816
+ return input_set
817
+
818
+ # If working with a list of managed inputs, we need to create an
819
+ # input set.
820
+ if scenario.scenario_input.scenario_input_type == ScenarioInputType.INPUT:
821
+ name, id = safe_name_and_id(prefix="inpset", entity_id=scenario_id)
822
+ input_set = self.new_input_set(
823
+ id=id,
824
+ name=name,
825
+ description=f"Automatically created from scenario test: {id}",
826
+ maximum_runs=20,
827
+ inputs=[
828
+ ManagedInput.from_dict(data={"id": input_id})
829
+ for input_id in scenario.scenario_input.scenario_input_data
830
+ ],
831
+ )
832
+ return input_set
833
+
834
+ # If working with new data, we need to create managed inputs, and then,
835
+ # an input set.
836
+ if scenario.scenario_input.scenario_input_type == ScenarioInputType.NEW:
837
+ managed_inputs = []
838
+ for data in scenario.scenario_input.scenario_input_data:
839
+ upload_url = self.upload_url()
840
+ self.upload_data(data=data, upload_url=upload_url)
841
+ name, id = safe_name_and_id(prefix="man-input", entity_id=scenario_id)
842
+ managed_input = self.new_managed_input(
843
+ id=id,
844
+ name=name,
845
+ description=f"Automatically created from scenario test: {id}",
846
+ upload_id=upload_url.upload_id,
847
+ )
848
+ managed_inputs.append(managed_input)
849
+
850
+ name, id = safe_name_and_id(prefix="inpset", entity_id=scenario_id)
851
+ input_set = self.new_input_set(
852
+ id=id,
853
+ name=name,
854
+ description=f"Automatically created from scenario test: {id}",
855
+ maximum_runs=20,
856
+ inputs=managed_inputs,
857
+ )
858
+ return input_set
859
+
860
+ raise ValueError(f"Unknown scenario input type: {scenario.scenario_input.scenario_input_type}")