nextmv 0.23.0__py3-none-any.whl → 0.25.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.
- nextmv/__about__.py +1 -1
- nextmv/__entrypoint__.py +7 -10
- nextmv/__init__.py +3 -0
- nextmv/cloud/__init__.py +7 -1
- nextmv/cloud/application.py +555 -50
- nextmv/cloud/batch_experiment.py +58 -22
- nextmv/cloud/client.py +2 -0
- nextmv/cloud/input_set.py +26 -0
- nextmv/cloud/manifest.py +157 -8
- nextmv/cloud/run.py +8 -7
- nextmv/cloud/safe.py +83 -0
- nextmv/cloud/scenario.py +229 -0
- nextmv/deprecated.py +13 -0
- nextmv/input.py +74 -0
- nextmv/options.py +293 -78
- nextmv/output.py +64 -7
- {nextmv-0.23.0.dist-info → nextmv-0.25.0.dist-info}/METADATA +1 -1
- nextmv-0.25.0.dist-info/RECORD +30 -0
- nextmv-0.23.0.dist-info/RECORD +0 -27
- {nextmv-0.23.0.dist-info → nextmv-0.25.0.dist-info}/WHEEL +0 -0
- {nextmv-0.23.0.dist-info → nextmv-0.25.0.dist-info}/licenses/LICENSE +0 -0
nextmv/cloud/application.py
CHANGED
|
@@ -16,10 +16,21 @@ from nextmv.cloud import package
|
|
|
16
16
|
from nextmv.cloud.acceptance_test import AcceptanceTest, ExperimentStatus, Metric
|
|
17
17
|
from nextmv.cloud.batch_experiment import BatchExperiment, BatchExperimentMetadata, BatchExperimentRun
|
|
18
18
|
from nextmv.cloud.client import Client, get_size
|
|
19
|
-
from nextmv.cloud.input_set import InputSet
|
|
19
|
+
from nextmv.cloud.input_set import InputSet, ManagedInput
|
|
20
20
|
from nextmv.cloud.instance import Instance, InstanceConfiguration
|
|
21
21
|
from nextmv.cloud.manifest import Manifest
|
|
22
|
-
from nextmv.cloud.run import
|
|
22
|
+
from nextmv.cloud.run import (
|
|
23
|
+
ExternalRunResult,
|
|
24
|
+
Format,
|
|
25
|
+
FormatInput,
|
|
26
|
+
RunConfiguration,
|
|
27
|
+
RunInformation,
|
|
28
|
+
RunLog,
|
|
29
|
+
RunResult,
|
|
30
|
+
TrackedRun,
|
|
31
|
+
)
|
|
32
|
+
from nextmv.cloud.safe import name_and_id
|
|
33
|
+
from nextmv.cloud.scenario import Scenario, ScenarioInputType, _option_sets, _scenarios_by_id
|
|
23
34
|
from nextmv.cloud.secrets import Secret, SecretsCollection, SecretsCollectionSummary
|
|
24
35
|
from nextmv.cloud.status import StatusV2
|
|
25
36
|
from nextmv.cloud.version import Version
|
|
@@ -230,11 +241,15 @@ class Application:
|
|
|
230
241
|
Deletes a batch experiment, along with all the associated information,
|
|
231
242
|
such as its runs.
|
|
232
243
|
|
|
233
|
-
|
|
234
|
-
|
|
244
|
+
Parameters
|
|
245
|
+
----------
|
|
246
|
+
batch_id: str
|
|
247
|
+
ID of the batch experiment.
|
|
235
248
|
|
|
236
|
-
Raises
|
|
237
|
-
|
|
249
|
+
Raises
|
|
250
|
+
------
|
|
251
|
+
requests.HTTPError
|
|
252
|
+
If the response status code is not 2xx.
|
|
238
253
|
"""
|
|
239
254
|
|
|
240
255
|
_ = self.client.request(
|
|
@@ -242,6 +257,24 @@ class Application:
|
|
|
242
257
|
endpoint=f"{self.experiments_endpoint}/batch/{batch_id}",
|
|
243
258
|
)
|
|
244
259
|
|
|
260
|
+
def delete_scenario_test(self, scenario_test_id: str) -> None:
|
|
261
|
+
"""
|
|
262
|
+
Deletes a scenario test. Scenario tests are based on the batch
|
|
263
|
+
experiments API, so this function summons `delete_batch_experiment`.
|
|
264
|
+
|
|
265
|
+
Parameters
|
|
266
|
+
----------
|
|
267
|
+
scenario_test_id: str
|
|
268
|
+
ID of the scenario test.
|
|
269
|
+
|
|
270
|
+
Raises
|
|
271
|
+
------
|
|
272
|
+
requests.HTTPError
|
|
273
|
+
If the response status code is not 2xx.
|
|
274
|
+
"""
|
|
275
|
+
|
|
276
|
+
self.delete_batch_experiment(batch_id=scenario_test_id)
|
|
277
|
+
|
|
245
278
|
def delete_secrets_collection(self, secrets_collection_id: str) -> None:
|
|
246
279
|
"""
|
|
247
280
|
Deletes a secrets collection.
|
|
@@ -359,16 +392,21 @@ class Application:
|
|
|
359
392
|
"""
|
|
360
393
|
List all batch experiments.
|
|
361
394
|
|
|
362
|
-
Returns
|
|
395
|
+
Returns
|
|
396
|
+
-------
|
|
397
|
+
list[BatchExperimentMetadata]
|
|
363
398
|
List of batch experiments.
|
|
364
399
|
|
|
365
|
-
Raises
|
|
366
|
-
|
|
400
|
+
Raises
|
|
401
|
+
------
|
|
402
|
+
requests.HTTPError
|
|
403
|
+
If the response status code is not 2xx.
|
|
367
404
|
"""
|
|
368
405
|
|
|
369
406
|
response = self.client.request(
|
|
370
407
|
method="GET",
|
|
371
408
|
endpoint=f"{self.experiments_endpoint}/batch",
|
|
409
|
+
query_params={"type": "batch"},
|
|
372
410
|
)
|
|
373
411
|
|
|
374
412
|
return [BatchExperimentMetadata.from_dict(batch_experiment) for batch_experiment in response.json()]
|
|
@@ -409,6 +447,53 @@ class Application:
|
|
|
409
447
|
|
|
410
448
|
return [Instance.from_dict(instance) for instance in response.json()]
|
|
411
449
|
|
|
450
|
+
def list_managed_inputs(self) -> list[ManagedInput]:
|
|
451
|
+
"""
|
|
452
|
+
List all managed inputs.
|
|
453
|
+
|
|
454
|
+
Returns
|
|
455
|
+
-------
|
|
456
|
+
list[ManagedInput]
|
|
457
|
+
List of managed inputs.
|
|
458
|
+
|
|
459
|
+
Raises
|
|
460
|
+
------
|
|
461
|
+
requests.HTTPError
|
|
462
|
+
If the response status code is not 2xx.
|
|
463
|
+
"""
|
|
464
|
+
|
|
465
|
+
response = self.client.request(
|
|
466
|
+
method="GET",
|
|
467
|
+
endpoint=f"{self.endpoint}/inputs",
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
return [ManagedInput.from_dict(managed_input) for managed_input in response.json()]
|
|
471
|
+
|
|
472
|
+
def list_scenario_tests(self) -> list[BatchExperimentMetadata]:
|
|
473
|
+
"""
|
|
474
|
+
List all batch scenario tests. Scenario tests are based on the batch
|
|
475
|
+
experiments API, so this function returns the same information as
|
|
476
|
+
`list_batch_experiments`, albeit using a different query parameter.
|
|
477
|
+
|
|
478
|
+
Returns
|
|
479
|
+
-------
|
|
480
|
+
list[BatchExperimentMetadata]
|
|
481
|
+
List of scenario tests.
|
|
482
|
+
|
|
483
|
+
Raises
|
|
484
|
+
------
|
|
485
|
+
requests.HTTPError
|
|
486
|
+
If the response status code is not 2xx.
|
|
487
|
+
"""
|
|
488
|
+
|
|
489
|
+
response = self.client.request(
|
|
490
|
+
method="GET",
|
|
491
|
+
endpoint=f"{self.experiments_endpoint}/batch",
|
|
492
|
+
query_params={"type": "scenario"},
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
return [BatchExperimentMetadata.from_dict(batch_experiment) for batch_experiment in response.json()]
|
|
496
|
+
|
|
412
497
|
def list_secrets_collections(self) -> list[SecretsCollectionSummary]:
|
|
413
498
|
"""
|
|
414
499
|
List all secrets collections.
|
|
@@ -445,6 +530,33 @@ class Application:
|
|
|
445
530
|
|
|
446
531
|
return [Version.from_dict(version) for version in response.json()]
|
|
447
532
|
|
|
533
|
+
def managed_input(self, managed_input_id: str) -> ManagedInput:
|
|
534
|
+
"""
|
|
535
|
+
Get a managed input.
|
|
536
|
+
|
|
537
|
+
Parameters
|
|
538
|
+
----------
|
|
539
|
+
managed_input_id: str
|
|
540
|
+
ID of the managed input.
|
|
541
|
+
|
|
542
|
+
Returns
|
|
543
|
+
-------
|
|
544
|
+
ManagedInput
|
|
545
|
+
The managed input.
|
|
546
|
+
|
|
547
|
+
Raises
|
|
548
|
+
------
|
|
549
|
+
requests.HTTPError
|
|
550
|
+
If the response status code is not 2xx.
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
response = self.client.request(
|
|
554
|
+
method="GET",
|
|
555
|
+
endpoint=f"{self.endpoint}/inputs/{managed_input_id}",
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
return ManagedInput.from_dict(response.json())
|
|
559
|
+
|
|
448
560
|
@classmethod
|
|
449
561
|
def new(
|
|
450
562
|
cls,
|
|
@@ -640,37 +752,58 @@ class Application:
|
|
|
640
752
|
def new_batch_experiment(
|
|
641
753
|
self,
|
|
642
754
|
name: str,
|
|
643
|
-
input_set_id: str,
|
|
644
|
-
instance_ids: list[str] = None,
|
|
755
|
+
input_set_id: Optional[str] = None,
|
|
756
|
+
instance_ids: Optional[list[str]] = None,
|
|
645
757
|
description: Optional[str] = None,
|
|
646
758
|
id: Optional[str] = None,
|
|
647
759
|
option_sets: Optional[dict[str, dict[str, str]]] = None,
|
|
648
760
|
runs: Optional[list[Union[BatchExperimentRun, dict[str, Any]]]] = None,
|
|
761
|
+
type: Optional[str] = "batch",
|
|
649
762
|
) -> str:
|
|
650
763
|
"""
|
|
651
764
|
Create a new batch experiment.
|
|
652
765
|
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
766
|
+
Parameters
|
|
767
|
+
----------
|
|
768
|
+
name: str
|
|
769
|
+
Name of the batch experiment.
|
|
770
|
+
input_set_id: str
|
|
771
|
+
ID of the input set to use for the batch experiment.
|
|
772
|
+
instance_ids: list[str]
|
|
773
|
+
List of instance IDs to use for the batch experiment.
|
|
774
|
+
description: Optional[str]
|
|
775
|
+
Optional description of the batch experiment.
|
|
776
|
+
id: Optional[str]
|
|
777
|
+
ID of the batch experiment. Will be generated if not provided.
|
|
778
|
+
option_sets: Optional[dict[str, dict[str, str]]]
|
|
779
|
+
Option sets to use for the batch experiment. This is a dictionary
|
|
780
|
+
where the keys are option set IDs and the values are dictionaries
|
|
781
|
+
with the actual options.
|
|
782
|
+
runs: Optional[list[BatchExperimentRun]]
|
|
783
|
+
List of runs to use for the batch experiment.
|
|
784
|
+
type: Optional[str]
|
|
785
|
+
Type of the batch experiment. This is used to determine the
|
|
786
|
+
experiment type. The default value is "batch". If you want to
|
|
787
|
+
create a scenario test, set this to "scenario".
|
|
661
788
|
|
|
662
|
-
Returns
|
|
789
|
+
Returns
|
|
790
|
+
-------
|
|
791
|
+
str
|
|
663
792
|
ID of the batch experiment.
|
|
664
793
|
|
|
665
|
-
Raises
|
|
666
|
-
|
|
794
|
+
Raises
|
|
795
|
+
------
|
|
796
|
+
requests.HTTPError
|
|
797
|
+
If the response status code is not 2xx.
|
|
667
798
|
"""
|
|
668
799
|
|
|
669
800
|
payload = {
|
|
670
801
|
"name": name,
|
|
671
|
-
"input_set_id": input_set_id,
|
|
672
|
-
"instance_ids": instance_ids,
|
|
673
802
|
}
|
|
803
|
+
if input_set_id is not None:
|
|
804
|
+
payload["input_set_id"] = input_set_id
|
|
805
|
+
if instance_ids is not None:
|
|
806
|
+
payload["instance_ids"] = instance_ids
|
|
674
807
|
if description is not None:
|
|
675
808
|
payload["description"] = description
|
|
676
809
|
if id is not None:
|
|
@@ -682,6 +815,8 @@ class Application:
|
|
|
682
815
|
for i, run in enumerate(runs):
|
|
683
816
|
payload_runs[i] = run.to_dict() if isinstance(run, BatchExperimentRun) else run
|
|
684
817
|
payload["runs"] = payload_runs
|
|
818
|
+
if type is not None:
|
|
819
|
+
payload["type"] = type
|
|
685
820
|
|
|
686
821
|
response = self.client.request(
|
|
687
822
|
method="POST",
|
|
@@ -701,26 +836,61 @@ class Application:
|
|
|
701
836
|
maximum_runs: Optional[int] = None,
|
|
702
837
|
run_ids: Optional[list[str]] = None,
|
|
703
838
|
start_time: Optional[datetime] = None,
|
|
839
|
+
inputs: Optional[list[ManagedInput]] = None,
|
|
704
840
|
) -> InputSet:
|
|
705
841
|
"""
|
|
706
|
-
Create a new input set.
|
|
842
|
+
Create a new input set. You can create an input set from three
|
|
843
|
+
different methodologies:
|
|
707
844
|
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
start_time: Start time of the runs to construct the input set.
|
|
845
|
+
1. Using `instance_id`, `start_time`, `end_time` and `maximum_runs`.
|
|
846
|
+
Instance runs will be obtained from the application matching the
|
|
847
|
+
criteria of dates and maximum number of runs.
|
|
848
|
+
2. Using `run_ids`. The input set will be created using the list of
|
|
849
|
+
runs specified by the user.
|
|
850
|
+
3. Using `inputs`. The input set will be created using the list of
|
|
851
|
+
inputs specified by the user. This is useful for creating an input
|
|
852
|
+
set from a list of inputs that are already available in the
|
|
853
|
+
application.
|
|
718
854
|
|
|
719
|
-
|
|
720
|
-
|
|
855
|
+
Parameters
|
|
856
|
+
----------
|
|
857
|
+
id: str
|
|
858
|
+
ID of the input set
|
|
859
|
+
name: str
|
|
860
|
+
Name of the input set.
|
|
861
|
+
description: Optional[str]
|
|
862
|
+
Optional description of the input set.
|
|
863
|
+
end_time: Optional[datetime]
|
|
864
|
+
End time of the input set. This is used to filter the runs
|
|
865
|
+
associated with the input set.
|
|
866
|
+
instance_id: Optional[str]
|
|
867
|
+
ID of the instance to use for the input set. This is used to
|
|
868
|
+
filter the runs associated with the input set. If not provided,
|
|
869
|
+
the application’s `default_instance_id` is used.
|
|
870
|
+
maximum_runs: Optional[int]
|
|
871
|
+
Maximum number of runs to use for the input set. This is used to
|
|
872
|
+
filter the runs associated with the input set. If not provided,
|
|
873
|
+
all runs are used.
|
|
874
|
+
run_ids: Optional[list[str]]
|
|
875
|
+
List of run IDs to use for the input set.
|
|
876
|
+
start_time: Optional[datetime]
|
|
877
|
+
Start time of the input set. This is used to filter the runs
|
|
878
|
+
associated with the input set.
|
|
879
|
+
inputs: Optional[list[ExperimentInput]]
|
|
880
|
+
List of inputs to use for the input set. This is used to create
|
|
881
|
+
the input set from a list of inputs that are already available in
|
|
882
|
+
the application.
|
|
721
883
|
|
|
722
|
-
|
|
723
|
-
|
|
884
|
+
|
|
885
|
+
Returns
|
|
886
|
+
-------
|
|
887
|
+
InputSet
|
|
888
|
+
The new input set.
|
|
889
|
+
|
|
890
|
+
Raises
|
|
891
|
+
------
|
|
892
|
+
requests.HTTPError
|
|
893
|
+
If the response status code is not 2xx.
|
|
724
894
|
"""
|
|
725
895
|
|
|
726
896
|
payload = {
|
|
@@ -739,6 +909,8 @@ class Application:
|
|
|
739
909
|
payload["run_ids"] = run_ids
|
|
740
910
|
if start_time is not None:
|
|
741
911
|
payload["start_time"] = start_time.isoformat()
|
|
912
|
+
if inputs is not None:
|
|
913
|
+
payload["inputs"] = [input.to_dict() for input in inputs]
|
|
742
914
|
|
|
743
915
|
response = self.client.request(
|
|
744
916
|
method="POST",
|
|
@@ -794,6 +966,84 @@ class Application:
|
|
|
794
966
|
|
|
795
967
|
return Instance.from_dict(response.json())
|
|
796
968
|
|
|
969
|
+
def new_managed_input(
|
|
970
|
+
self,
|
|
971
|
+
id: str,
|
|
972
|
+
name: str,
|
|
973
|
+
description: Optional[str] = None,
|
|
974
|
+
upload_id: Optional[str] = None,
|
|
975
|
+
run_id: Optional[str] = None,
|
|
976
|
+
format: Optional[Union[Format, dict[str, any]]] = None,
|
|
977
|
+
) -> ManagedInput:
|
|
978
|
+
"""
|
|
979
|
+
Create a new managed input. There are two methods for creating a
|
|
980
|
+
managed input:
|
|
981
|
+
|
|
982
|
+
1. Specifying the `upload_id` parameter. You may use the `upload_url`
|
|
983
|
+
method to obtain the upload ID and the `upload_large_input` method
|
|
984
|
+
to upload the data to it.
|
|
985
|
+
2. Specifying the `run_id` parameter. The managed input will be
|
|
986
|
+
created from the run specified by the `run_id` parameter.
|
|
987
|
+
|
|
988
|
+
Either the `upload_id` or the `run_id` parameter must be specified.
|
|
989
|
+
|
|
990
|
+
Parameters
|
|
991
|
+
----------
|
|
992
|
+
id: str
|
|
993
|
+
ID of the managed input.
|
|
994
|
+
name: str
|
|
995
|
+
Name of the managed input.
|
|
996
|
+
description: Optional[str]
|
|
997
|
+
Optional description of the managed input.
|
|
998
|
+
upload_id: Optional[str]
|
|
999
|
+
ID of the upload to use for the managed input.
|
|
1000
|
+
run_id: Optional[str]
|
|
1001
|
+
ID of the run to use for the managed input.
|
|
1002
|
+
format: Optional[Format]
|
|
1003
|
+
Format of the managed input. Default will be formatted as `JSON`.
|
|
1004
|
+
|
|
1005
|
+
Returns
|
|
1006
|
+
-------
|
|
1007
|
+
ManagedInput
|
|
1008
|
+
The new managed input.
|
|
1009
|
+
|
|
1010
|
+
Raises
|
|
1011
|
+
------
|
|
1012
|
+
requests.HTTPError
|
|
1013
|
+
If the response status code is not 2xx.
|
|
1014
|
+
ValueError
|
|
1015
|
+
If neither the `upload_id` nor the `run_id` parameter is
|
|
1016
|
+
specified.
|
|
1017
|
+
"""
|
|
1018
|
+
|
|
1019
|
+
if upload_id is None and run_id is None:
|
|
1020
|
+
raise ValueError("Either upload_id or run_id must be specified")
|
|
1021
|
+
|
|
1022
|
+
payload = {
|
|
1023
|
+
"id": id,
|
|
1024
|
+
"name": name,
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if description is not None:
|
|
1028
|
+
payload["description"] = description
|
|
1029
|
+
if upload_id is not None:
|
|
1030
|
+
payload["upload_id"] = upload_id
|
|
1031
|
+
if run_id is not None:
|
|
1032
|
+
payload["run_id"] = run_id
|
|
1033
|
+
|
|
1034
|
+
if format is not None:
|
|
1035
|
+
payload["format"] = format.to_dict() if isinstance(format, Format) else format
|
|
1036
|
+
else:
|
|
1037
|
+
payload["format"] = Format(format_input=FormatInput(input_type=InputFormat.JSON)).to_dict()
|
|
1038
|
+
|
|
1039
|
+
response = self.client.request(
|
|
1040
|
+
method="POST",
|
|
1041
|
+
endpoint=f"{self.endpoint}/inputs",
|
|
1042
|
+
payload=payload,
|
|
1043
|
+
)
|
|
1044
|
+
|
|
1045
|
+
return ManagedInput.from_dict(response.json())
|
|
1046
|
+
|
|
797
1047
|
def new_run( # noqa: C901 # Refactor this function at some point.
|
|
798
1048
|
self,
|
|
799
1049
|
input: Union[Input, dict[str, Any], BaseModel, str] = None,
|
|
@@ -893,11 +1143,11 @@ class Application:
|
|
|
893
1143
|
|
|
894
1144
|
options_dict = {}
|
|
895
1145
|
if isinstance(input, Input) and input.options is not None:
|
|
896
|
-
options_dict = input.options.
|
|
1146
|
+
options_dict = input.options.to_dict_cloud()
|
|
897
1147
|
|
|
898
1148
|
if options is not None:
|
|
899
1149
|
if isinstance(options, Options):
|
|
900
|
-
options_dict = options.
|
|
1150
|
+
options_dict = options.to_dict_cloud()
|
|
901
1151
|
elif isinstance(options, dict):
|
|
902
1152
|
for k, v in options.items():
|
|
903
1153
|
if isinstance(v, str):
|
|
@@ -1048,6 +1298,127 @@ class Application:
|
|
|
1048
1298
|
polling_options=polling_options,
|
|
1049
1299
|
)
|
|
1050
1300
|
|
|
1301
|
+
def new_scenario_test(
|
|
1302
|
+
self,
|
|
1303
|
+
id: str,
|
|
1304
|
+
name: str,
|
|
1305
|
+
scenarios: list[Scenario],
|
|
1306
|
+
description: Optional[str] = None,
|
|
1307
|
+
repetitions: Optional[int] = 0,
|
|
1308
|
+
) -> str:
|
|
1309
|
+
"""
|
|
1310
|
+
Create a new scenario test. The test is based on `scenarios` and you
|
|
1311
|
+
may specify `repetitions` to run the test multiple times. 0 repetitions
|
|
1312
|
+
means that the tests will be executed once. 1 repetition means that the
|
|
1313
|
+
test will be repeated once, i.e.: it will be executed twice. 2
|
|
1314
|
+
repetitions equals 3 executions, so on, and so forth.
|
|
1315
|
+
|
|
1316
|
+
For each scenario, consider the `scenario_input` and `configuration`.
|
|
1317
|
+
The `scenario_input.scenario_input_type` allows you to specify the data
|
|
1318
|
+
that will be used for that scenario.
|
|
1319
|
+
|
|
1320
|
+
- `ScenarioInputType.INPUT_SET`: the data should be taken from an
|
|
1321
|
+
existing input set.
|
|
1322
|
+
- `ScenarioInputType.INPUT`: the data should be taken from a list of
|
|
1323
|
+
existing inputs. When using this type, an input set will be created
|
|
1324
|
+
from this set of managed inputs.
|
|
1325
|
+
- `ScenarioInputType.New`: a new set of data will be uploaded as a set
|
|
1326
|
+
of managed inputs. A new input set will be created from this set of
|
|
1327
|
+
managed inputs.
|
|
1328
|
+
|
|
1329
|
+
On the other hand, the `configuration` allows you to specify multiple
|
|
1330
|
+
option variations for the scenario. Please see the
|
|
1331
|
+
`ScenarioConfiguration` class for more information.
|
|
1332
|
+
|
|
1333
|
+
The scenario tests uses the batch experiments API under the hood.
|
|
1334
|
+
|
|
1335
|
+
Parameters
|
|
1336
|
+
----------
|
|
1337
|
+
id: str
|
|
1338
|
+
ID of the scenario test.
|
|
1339
|
+
name: str
|
|
1340
|
+
Name of the scenario test.
|
|
1341
|
+
scenarios: list[Scenario]
|
|
1342
|
+
List of scenarios to use for the scenario test. At least one
|
|
1343
|
+
scenario should be provided.
|
|
1344
|
+
description: Optional[str]
|
|
1345
|
+
Optional description of the scenario test.
|
|
1346
|
+
repetitions: Optional[int]
|
|
1347
|
+
Number of repetitions to use for the scenario test. 0
|
|
1348
|
+
repetitions means that the tests will be executed once. 1
|
|
1349
|
+
repetition means that the test will be repeated once, i.e.: it
|
|
1350
|
+
will be executed twice. 2 repetitions equals 3 executions, so on,
|
|
1351
|
+
and so forth.
|
|
1352
|
+
|
|
1353
|
+
Returns
|
|
1354
|
+
-------
|
|
1355
|
+
str
|
|
1356
|
+
ID of the scenario test.
|
|
1357
|
+
|
|
1358
|
+
Raises
|
|
1359
|
+
------
|
|
1360
|
+
requests.HTTPError
|
|
1361
|
+
If the response status code is not 2xx.
|
|
1362
|
+
ValueError
|
|
1363
|
+
If no scenarios are provided.
|
|
1364
|
+
"""
|
|
1365
|
+
|
|
1366
|
+
if len(scenarios) < 1:
|
|
1367
|
+
raise ValueError("At least one scenario must be provided")
|
|
1368
|
+
|
|
1369
|
+
scenarios_by_id = _scenarios_by_id(scenarios)
|
|
1370
|
+
|
|
1371
|
+
# Save all the information needed by scenario.
|
|
1372
|
+
input_sets = {}
|
|
1373
|
+
instances = {}
|
|
1374
|
+
for scenario_id, scenario in scenarios_by_id.items():
|
|
1375
|
+
instance = self.instance(instance_id=scenario.instance_id)
|
|
1376
|
+
|
|
1377
|
+
# Each scenario is associated to an input set, so we must either
|
|
1378
|
+
# get it or create it.
|
|
1379
|
+
input_set = self.__input_set_for_scenario(scenario, scenario_id)
|
|
1380
|
+
|
|
1381
|
+
instances[scenario_id] = instance
|
|
1382
|
+
input_sets[scenario_id] = input_set
|
|
1383
|
+
|
|
1384
|
+
# Calculate the combinations of all the option sets across scenarios.
|
|
1385
|
+
opt_sets_by_scenario = _option_sets(scenarios)
|
|
1386
|
+
|
|
1387
|
+
# The scenario tests results in multiple individual runs.
|
|
1388
|
+
runs = []
|
|
1389
|
+
run_counter = 0
|
|
1390
|
+
opt_sets = {}
|
|
1391
|
+
for scenario_id, scenario_opt_sets in opt_sets_by_scenario.items():
|
|
1392
|
+
opt_sets = {**opt_sets, **scenario_opt_sets}
|
|
1393
|
+
input_set = input_sets[scenario_id]
|
|
1394
|
+
scenario = scenarios_by_id[scenario_id]
|
|
1395
|
+
|
|
1396
|
+
for set_key in scenario_opt_sets.keys():
|
|
1397
|
+
inputs = input_set.input_ids if len(input_set.input_ids) > 0 else input_set.inputs
|
|
1398
|
+
for input in inputs:
|
|
1399
|
+
input_id = input.id if isinstance(input, ManagedInput) else input
|
|
1400
|
+
for repetition in range(repetitions + 1):
|
|
1401
|
+
run_counter += 1
|
|
1402
|
+
run = BatchExperimentRun(
|
|
1403
|
+
input_id=input_id,
|
|
1404
|
+
input_set_id=input_set.id,
|
|
1405
|
+
instance_id=scenario.instance_id,
|
|
1406
|
+
option_set=set_key,
|
|
1407
|
+
scenario_id=scenario_id,
|
|
1408
|
+
repetition=repetition,
|
|
1409
|
+
run_number=f"{run_counter}",
|
|
1410
|
+
)
|
|
1411
|
+
runs.append(run)
|
|
1412
|
+
|
|
1413
|
+
return self.new_batch_experiment(
|
|
1414
|
+
id=id,
|
|
1415
|
+
name=name,
|
|
1416
|
+
description=description,
|
|
1417
|
+
type="scenario",
|
|
1418
|
+
option_sets=opt_sets,
|
|
1419
|
+
runs=runs,
|
|
1420
|
+
)
|
|
1421
|
+
|
|
1051
1422
|
def new_secrets_collection(
|
|
1052
1423
|
self,
|
|
1053
1424
|
secrets: list[Secret],
|
|
@@ -1201,12 +1572,12 @@ class Application:
|
|
|
1201
1572
|
|
|
1202
1573
|
|
|
1203
1574
|
# Define the options that the model needs.
|
|
1204
|
-
|
|
1575
|
+
opt = []
|
|
1205
1576
|
default_options = nextroute.Options()
|
|
1206
1577
|
for name, default_value in default_options.to_dict().items():
|
|
1207
|
-
|
|
1578
|
+
opt.append(nextmv.Option(name.lower(), type(default_value), default_value, name, False))
|
|
1208
1579
|
|
|
1209
|
-
options = nextmv.Options(*
|
|
1580
|
+
options = nextmv.Options(*opt)
|
|
1210
1581
|
|
|
1211
1582
|
# Instantiate the model and model configuration.
|
|
1212
1583
|
model = DecisionModel()
|
|
@@ -1326,7 +1697,10 @@ class Application:
|
|
|
1326
1697
|
endpoint=f"{self.endpoint}/runs/{run_id}/metadata",
|
|
1327
1698
|
)
|
|
1328
1699
|
|
|
1329
|
-
|
|
1700
|
+
info = RunInformation.from_dict(response.json())
|
|
1701
|
+
info.console_url = self.__console_url(info.id)
|
|
1702
|
+
|
|
1703
|
+
return info
|
|
1330
1704
|
|
|
1331
1705
|
def run_logs(self, run_id: str) -> RunLog:
|
|
1332
1706
|
"""
|
|
@@ -1401,7 +1775,31 @@ class Application:
|
|
|
1401
1775
|
|
|
1402
1776
|
return self.__run_result(run_id=run_id, run_information=run_information)
|
|
1403
1777
|
|
|
1404
|
-
def
|
|
1778
|
+
def scenario_test(self, scenario_test_id: str) -> BatchExperiment:
|
|
1779
|
+
"""
|
|
1780
|
+
Get the scenario test. Scenario tests are based on batch experiments,
|
|
1781
|
+
so this function will return the corresponding batch experiment
|
|
1782
|
+
associated to the scenario test.
|
|
1783
|
+
|
|
1784
|
+
Parameters
|
|
1785
|
+
----------
|
|
1786
|
+
scenario_test_id : str
|
|
1787
|
+
ID of the scenario test.
|
|
1788
|
+
|
|
1789
|
+
Returns
|
|
1790
|
+
-------
|
|
1791
|
+
BatchExperiment
|
|
1792
|
+
The scenario test.
|
|
1793
|
+
|
|
1794
|
+
Raises
|
|
1795
|
+
------
|
|
1796
|
+
requests.HTTPError
|
|
1797
|
+
If the response status code is not 2xx.
|
|
1798
|
+
"""
|
|
1799
|
+
|
|
1800
|
+
return self.batch_experiment(batch_id=scenario_test_id)
|
|
1801
|
+
|
|
1802
|
+
def track_run(self, tracked_run: TrackedRun, instance_id: Optional[str] = None) -> str:
|
|
1405
1803
|
"""
|
|
1406
1804
|
Track an external run.
|
|
1407
1805
|
|
|
@@ -1414,6 +1812,9 @@ class Application:
|
|
|
1414
1812
|
----------
|
|
1415
1813
|
tracked_run : TrackedRun
|
|
1416
1814
|
The run to track.
|
|
1815
|
+
instance_id: Optional[str]
|
|
1816
|
+
Optional instance ID if you want to associate your tracked run with
|
|
1817
|
+
an instance.
|
|
1417
1818
|
|
|
1418
1819
|
Returns
|
|
1419
1820
|
-------
|
|
@@ -1431,7 +1832,7 @@ class Application:
|
|
|
1431
1832
|
|
|
1432
1833
|
upload_input = tracked_run.input
|
|
1433
1834
|
if isinstance(tracked_run.input, Input):
|
|
1434
|
-
upload_input = tracked_run.input.
|
|
1835
|
+
upload_input = tracked_run.input.data
|
|
1435
1836
|
|
|
1436
1837
|
self.upload_large_input(input=upload_input, upload_url=url_input)
|
|
1437
1838
|
|
|
@@ -1457,12 +1858,17 @@ class Application:
|
|
|
1457
1858
|
if tracked_run.error is not None and tracked_run.error != "":
|
|
1458
1859
|
external_result.error_message = tracked_run.error
|
|
1459
1860
|
|
|
1460
|
-
return self.new_run(
|
|
1861
|
+
return self.new_run(
|
|
1862
|
+
upload_id=url_input.upload_id,
|
|
1863
|
+
external_result=external_result,
|
|
1864
|
+
instance_id=instance_id,
|
|
1865
|
+
)
|
|
1461
1866
|
|
|
1462
1867
|
def track_run_with_result(
|
|
1463
1868
|
self,
|
|
1464
1869
|
tracked_run: TrackedRun,
|
|
1465
1870
|
polling_options: PollingOptions = _DEFAULT_POLLING_OPTIONS,
|
|
1871
|
+
instance_id: Optional[str] = None,
|
|
1466
1872
|
) -> RunResult:
|
|
1467
1873
|
"""
|
|
1468
1874
|
Track an external run and poll for the result. This is a convenience
|
|
@@ -1476,6 +1882,9 @@ class Application:
|
|
|
1476
1882
|
The run to track.
|
|
1477
1883
|
polling_options : PollingOptions
|
|
1478
1884
|
Options to use when polling for the run result.
|
|
1885
|
+
instance_id: Optional[str]
|
|
1886
|
+
Optional instance ID if you want to associate your tracked run with
|
|
1887
|
+
an instance.
|
|
1479
1888
|
|
|
1480
1889
|
Returns
|
|
1481
1890
|
-------
|
|
@@ -1495,7 +1904,7 @@ class Application:
|
|
|
1495
1904
|
If the run does not succeed after the polling strategy is
|
|
1496
1905
|
exhausted based on number of tries.
|
|
1497
1906
|
"""
|
|
1498
|
-
run_id = self.track_run(tracked_run=tracked_run)
|
|
1907
|
+
run_id = self.track_run(tracked_run=tracked_run, instance_id=instance_id)
|
|
1499
1908
|
|
|
1500
1909
|
return self.run_result_with_polling(
|
|
1501
1910
|
run_id=run_id,
|
|
@@ -1546,6 +1955,45 @@ class Application:
|
|
|
1546
1955
|
|
|
1547
1956
|
return Instance.from_dict(response.json())
|
|
1548
1957
|
|
|
1958
|
+
def update_managed_input(
|
|
1959
|
+
self,
|
|
1960
|
+
managed_input_id: str,
|
|
1961
|
+
name: str,
|
|
1962
|
+
description: str,
|
|
1963
|
+
) -> None:
|
|
1964
|
+
"""
|
|
1965
|
+
Update a managed input.
|
|
1966
|
+
|
|
1967
|
+
Parameters
|
|
1968
|
+
----------
|
|
1969
|
+
managed_input_id : str
|
|
1970
|
+
ID of the managed input to update.
|
|
1971
|
+
name : str
|
|
1972
|
+
Name of the managed input.
|
|
1973
|
+
description : str
|
|
1974
|
+
Description of the managed input.
|
|
1975
|
+
|
|
1976
|
+
Returns
|
|
1977
|
+
-------
|
|
1978
|
+
None
|
|
1979
|
+
No return value.
|
|
1980
|
+
|
|
1981
|
+
Raises
|
|
1982
|
+
------
|
|
1983
|
+
requests.HTTPError
|
|
1984
|
+
If the response status code is not 2xx.
|
|
1985
|
+
"""
|
|
1986
|
+
|
|
1987
|
+
payload = {
|
|
1988
|
+
"name": name,
|
|
1989
|
+
"description": description,
|
|
1990
|
+
}
|
|
1991
|
+
_ = self.client.request(
|
|
1992
|
+
method="PUT",
|
|
1993
|
+
endpoint=f"{self.endpoint}/inputs/{managed_input_id}",
|
|
1994
|
+
payload=payload,
|
|
1995
|
+
)
|
|
1996
|
+
|
|
1549
1997
|
def update_secrets_collection(
|
|
1550
1998
|
self,
|
|
1551
1999
|
secrets_collection_id: str,
|
|
@@ -1605,7 +2053,7 @@ class Application:
|
|
|
1605
2053
|
if isinstance(input, dict):
|
|
1606
2054
|
input = json.dumps(input)
|
|
1607
2055
|
|
|
1608
|
-
|
|
2056
|
+
self.client.upload_to_presigned_url(
|
|
1609
2057
|
url=upload_url.upload_url,
|
|
1610
2058
|
data=input,
|
|
1611
2059
|
)
|
|
@@ -1702,6 +2150,8 @@ class Application:
|
|
|
1702
2150
|
query_params=query_params,
|
|
1703
2151
|
)
|
|
1704
2152
|
result = RunResult.from_dict(response.json())
|
|
2153
|
+
result.console_url = self.__console_url(result.id)
|
|
2154
|
+
|
|
1705
2155
|
if not large_output:
|
|
1706
2156
|
return result
|
|
1707
2157
|
|
|
@@ -1766,6 +2216,61 @@ class Application:
|
|
|
1766
2216
|
)
|
|
1767
2217
|
)
|
|
1768
2218
|
|
|
2219
|
+
def __console_url(self, run_id: str) -> str:
|
|
2220
|
+
"""Auxiliary method to get the console URL for a run."""
|
|
2221
|
+
|
|
2222
|
+
return f"{self.client.console_url}/app/{self.id}/run/{run_id}?view=details"
|
|
2223
|
+
|
|
2224
|
+
def __input_set_for_scenario(self, scenario: Scenario, scenario_id: str) -> InputSet:
|
|
2225
|
+
# If working with an input set, there is no need to create one.
|
|
2226
|
+
if scenario.scenario_input.scenario_input_type == ScenarioInputType.INPUT_SET:
|
|
2227
|
+
input_set = self.input_set(input_set_id=scenario.scenario_input.scenario_input_data)
|
|
2228
|
+
return input_set
|
|
2229
|
+
|
|
2230
|
+
# If working with a list of managed inputs, we need to create an
|
|
2231
|
+
# input set.
|
|
2232
|
+
if scenario.scenario_input.scenario_input_type == ScenarioInputType.INPUT:
|
|
2233
|
+
name, id = name_and_id(prefix="inpset", entity_id=scenario_id)
|
|
2234
|
+
input_set = self.new_input_set(
|
|
2235
|
+
id=id,
|
|
2236
|
+
name=name,
|
|
2237
|
+
description=f"Automatically created from scenario test: {id}",
|
|
2238
|
+
maximum_runs=20,
|
|
2239
|
+
inputs=[
|
|
2240
|
+
ManagedInput.from_dict(data={"id": input_id})
|
|
2241
|
+
for input_id in scenario.scenario_input.scenario_input_data
|
|
2242
|
+
],
|
|
2243
|
+
)
|
|
2244
|
+
return input_set
|
|
2245
|
+
|
|
2246
|
+
# If working with new data, we need to create managed inputs, and then,
|
|
2247
|
+
# an input set.
|
|
2248
|
+
if scenario.scenario_input.scenario_input_type == ScenarioInputType.NEW:
|
|
2249
|
+
managed_inputs = []
|
|
2250
|
+
for data in scenario.scenario_input.scenario_input_data:
|
|
2251
|
+
upload_url = self.upload_url()
|
|
2252
|
+
self.upload_large_input(input=data, upload_url=upload_url)
|
|
2253
|
+
name, id = name_and_id(prefix="man-input", entity_id=scenario_id)
|
|
2254
|
+
managed_input = self.new_managed_input(
|
|
2255
|
+
id=id,
|
|
2256
|
+
name=name,
|
|
2257
|
+
description=f"Automatically created from scenario test: {id}",
|
|
2258
|
+
upload_id=upload_url.upload_id,
|
|
2259
|
+
)
|
|
2260
|
+
managed_inputs.append(managed_input)
|
|
2261
|
+
|
|
2262
|
+
name, id = name_and_id(prefix="inpset", entity_id=scenario_id)
|
|
2263
|
+
input_set = self.new_input_set(
|
|
2264
|
+
id=id,
|
|
2265
|
+
name=name,
|
|
2266
|
+
description=f"Automatically created from scenario test: {id}",
|
|
2267
|
+
maximum_runs=20,
|
|
2268
|
+
inputs=managed_inputs,
|
|
2269
|
+
)
|
|
2270
|
+
return input_set
|
|
2271
|
+
|
|
2272
|
+
raise ValueError(f"Unknown scenario input type: {scenario.scenario_input.scenario_input_type}")
|
|
2273
|
+
|
|
1769
2274
|
|
|
1770
2275
|
def poll(polling_options: PollingOptions, polling_func: Callable[[], tuple[any, bool]]) -> any:
|
|
1771
2276
|
"""
|