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