nextmv 0.32.0__tar.gz → 0.33.0__tar.gz

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 (88) hide show
  1. {nextmv-0.32.0 → nextmv-0.33.0}/PKG-INFO +1 -1
  2. nextmv-0.33.0/nextmv/__about__.py +1 -0
  3. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/__init__.py +1 -0
  4. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/cloud/__init__.py +7 -0
  5. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/cloud/acceptance_test.py +69 -5
  6. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/cloud/application.py +257 -95
  7. nextmv-0.33.0/nextmv/cloud/ensemble.py +248 -0
  8. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/cloud/package.py +62 -24
  9. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/local/executor.py +2 -1
  10. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/manifest.py +61 -2
  11. {nextmv-0.32.0 → nextmv-0.33.0}/tests/local/test_executor.py +2 -1
  12. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_manifest.py +5 -0
  13. nextmv-0.32.0/nextmv/__about__.py +0 -1
  14. {nextmv-0.32.0 → nextmv-0.33.0}/.gitignore +0 -0
  15. {nextmv-0.32.0 → nextmv-0.33.0}/LICENSE +0 -0
  16. {nextmv-0.32.0 → nextmv-0.33.0}/README.md +0 -0
  17. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/__entrypoint__.py +0 -0
  18. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/_serialization.py +0 -0
  19. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/base_model.py +0 -0
  20. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/cloud/account.py +0 -0
  21. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/cloud/batch_experiment.py +0 -0
  22. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/cloud/client.py +0 -0
  23. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/cloud/input_set.py +0 -0
  24. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/cloud/instance.py +0 -0
  25. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/cloud/scenario.py +0 -0
  26. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/cloud/secrets.py +0 -0
  27. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/cloud/url.py +0 -0
  28. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/cloud/version.py +0 -0
  29. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/default_app/.gitignore +0 -0
  30. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/default_app/README.md +0 -0
  31. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/default_app/app.yaml +0 -0
  32. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/default_app/input.json +0 -0
  33. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/default_app/main.py +0 -0
  34. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/default_app/requirements.txt +0 -0
  35. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/default_app/src/__init__.py +0 -0
  36. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/default_app/src/main.py +0 -0
  37. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/default_app/src/visuals.py +0 -0
  38. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/deprecated.py +0 -0
  39. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/input.py +0 -0
  40. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/local/__init__.py +0 -0
  41. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/local/application.py +0 -0
  42. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/local/geojson_handler.py +0 -0
  43. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/local/local.py +0 -0
  44. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/local/plotly_handler.py +0 -0
  45. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/local/runner.py +0 -0
  46. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/logger.py +0 -0
  47. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/model.py +0 -0
  48. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/options.py +0 -0
  49. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/output.py +0 -0
  50. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/polling.py +0 -0
  51. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/run.py +0 -0
  52. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/safe.py +0 -0
  53. {nextmv-0.32.0 → nextmv-0.33.0}/nextmv/status.py +0 -0
  54. {nextmv-0.32.0 → nextmv-0.33.0}/pyproject.toml +0 -0
  55. {nextmv-0.32.0 → nextmv-0.33.0}/tests/__init__.py +0 -0
  56. {nextmv-0.32.0 → nextmv-0.33.0}/tests/cloud/__init__.py +0 -0
  57. {nextmv-0.32.0 → nextmv-0.33.0}/tests/cloud/app.yaml +0 -0
  58. {nextmv-0.32.0 → nextmv-0.33.0}/tests/cloud/test_client.py +0 -0
  59. {nextmv-0.32.0 → nextmv-0.33.0}/tests/cloud/test_package.py +0 -0
  60. {nextmv-0.32.0 → nextmv-0.33.0}/tests/cloud/test_scenario.py +0 -0
  61. {nextmv-0.32.0 → nextmv-0.33.0}/tests/local/__init__.py +0 -0
  62. {nextmv-0.32.0 → nextmv-0.33.0}/tests/local/test_application.py +0 -0
  63. {nextmv-0.32.0 → nextmv-0.33.0}/tests/local/test_runner.py +0 -0
  64. {nextmv-0.32.0 → nextmv-0.33.0}/tests/scripts/__init__.py +0 -0
  65. {nextmv-0.32.0 → nextmv-0.33.0}/tests/scripts/options1.py +0 -0
  66. {nextmv-0.32.0 → nextmv-0.33.0}/tests/scripts/options2.py +0 -0
  67. {nextmv-0.32.0 → nextmv-0.33.0}/tests/scripts/options3.py +0 -0
  68. {nextmv-0.32.0 → nextmv-0.33.0}/tests/scripts/options4.py +0 -0
  69. {nextmv-0.32.0 → nextmv-0.33.0}/tests/scripts/options5.py +0 -0
  70. {nextmv-0.32.0 → nextmv-0.33.0}/tests/scripts/options6.py +0 -0
  71. {nextmv-0.32.0 → nextmv-0.33.0}/tests/scripts/options7.py +0 -0
  72. {nextmv-0.32.0 → nextmv-0.33.0}/tests/scripts/options_deprecated.py +0 -0
  73. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_base_model.py +0 -0
  74. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_entrypoint/__init__.py +0 -0
  75. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_entrypoint/test_entrypoint.py +0 -0
  76. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_input.py +0 -0
  77. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_inputs/test_data.csv +0 -0
  78. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_inputs/test_data.json +0 -0
  79. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_inputs/test_data.txt +0 -0
  80. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_logger.py +0 -0
  81. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_model.py +0 -0
  82. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_options.py +0 -0
  83. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_output.py +0 -0
  84. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_polling.py +0 -0
  85. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_run.py +0 -0
  86. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_safe.py +0 -0
  87. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_serialization.py +0 -0
  88. {nextmv-0.32.0 → nextmv-0.33.0}/tests/test_version.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nextmv
3
- Version: 0.32.0
3
+ Version: 0.33.0
4
4
  Summary: The all-purpose Python SDK for Nextmv
5
5
  Project-URL: Homepage, https://www.nextmv.io
6
6
  Project-URL: Documentation, https://nextmv-py.docs.nextmv.io/en/latest/nextmv/
@@ -0,0 +1 @@
1
+ __version__ = "v0.33.0"
@@ -21,6 +21,7 @@ from .manifest import Manifest as Manifest
21
21
  from .manifest import ManifestBuild as ManifestBuild
22
22
  from .manifest import ManifestOption as ManifestOption
23
23
  from .manifest import ManifestPython as ManifestPython
24
+ from .manifest import ManifestPythonArch as ManifestPythonArch
24
25
  from .manifest import ManifestPythonModel as ManifestPythonModel
25
26
  from .manifest import ManifestRuntime as ManifestRuntime
26
27
  from .manifest import ManifestType as ManifestType
@@ -47,6 +47,7 @@ from .acceptance_test import MetricParams as MetricParams
47
47
  from .acceptance_test import MetricResult as MetricResult
48
48
  from .acceptance_test import MetricStatistics as MetricStatistics
49
49
  from .acceptance_test import MetricTolerance as MetricTolerance
50
+ from .acceptance_test import MetricToleranceType as MetricToleranceType
50
51
  from .acceptance_test import MetricType as MetricType
51
52
  from .acceptance_test import ResultStatistics as ResultStatistics
52
53
  from .acceptance_test import StatisticType as StatisticType
@@ -63,6 +64,12 @@ from .batch_experiment import BatchExperimentRun as BatchExperimentRun
63
64
  from .batch_experiment import ExperimentStatus as ExperimentStatus
64
65
  from .client import Client as Client
65
66
  from .client import get_size as get_size
67
+ from .ensemble import EnsembleDefinition as EnsembleDefinition
68
+ from .ensemble import EvaluationRule as EvaluationRule
69
+ from .ensemble import RuleObjective as RuleObjective
70
+ from .ensemble import RuleTolerance as RuleTolerance
71
+ from .ensemble import RuleToleranceType as RuleToleranceType
72
+ from .ensemble import RunGroup as RunGroup
66
73
  from .input_set import InputSet as InputSet
67
74
  from .input_set import ManagedInput as ManagedInput
68
75
  from .instance import Instance as Instance
@@ -47,6 +47,7 @@ from typing import Optional
47
47
 
48
48
  from nextmv.base_model import BaseModel
49
49
  from nextmv.cloud.batch_experiment import ExperimentStatus
50
+ from nextmv.deprecated import deprecated
50
51
 
51
52
 
52
53
  class MetricType(str, Enum):
@@ -214,6 +215,9 @@ class Comparison(str, Enum):
214
215
 
215
216
  class ToleranceType(str, Enum):
216
217
  """
218
+ !!! warning
219
+ `ToleranceType` is deprecated, use `MetricToleranceType` instead.
220
+
217
221
  Type of tolerance used for a metric.
218
222
 
219
223
  You can import the `ToleranceType` class directly from `cloud`:
@@ -242,6 +246,66 @@ class ToleranceType(str, Enum):
242
246
  <ToleranceType.absolute: 'absolute'>
243
247
  """
244
248
 
249
+ undefined = ""
250
+ """ToleranceType is deprecated, please use MetricToleranceType instead.
251
+ Undefined tolerance type."""
252
+ absolute = "absolute"
253
+ """ToleranceType is deprecated, please use MetricToleranceType instead.
254
+ Absolute tolerance type."""
255
+ relative = "relative"
256
+ """ToleranceType is deprecated, please use MetricToleranceType instead.
257
+ Relative tolerance type."""
258
+
259
+
260
+ # Override __getattribute__ to emit deprecation warnings when enum values are accessed
261
+ _original_getattribute = ToleranceType.__class__.__getattribute__
262
+
263
+
264
+ def _deprecated_getattribute(cls, name: str):
265
+ # Only emit deprecation warning if this is specifically the ToleranceType class
266
+ if cls is ToleranceType and name in ("undefined", "absolute", "relative"):
267
+ deprecated(
268
+ f"ToleranceType.{name}",
269
+ "ToleranceType is deprecated and will be removed in a future version. "
270
+ "Please use MetricToleranceType instead",
271
+ )
272
+
273
+ return _original_getattribute(cls, name)
274
+
275
+
276
+ ToleranceType.__class__.__getattribute__ = _deprecated_getattribute
277
+
278
+
279
+ class MetricToleranceType(str, Enum):
280
+ """
281
+ Type of tolerance used for a metric.
282
+
283
+ You can import the `MetricToleranceType` class directly from `cloud`:
284
+
285
+ ```python
286
+ from nextmv.cloud import MetricToleranceType
287
+ ```
288
+
289
+ This enumeration defines the different types of tolerances that can be used
290
+ when comparing metrics in acceptance tests.
291
+
292
+ Attributes
293
+ ----------
294
+ undefined : str
295
+ Undefined tolerance type (empty string).
296
+ absolute : str
297
+ Absolute tolerance type, using a fixed value.
298
+ relative : str
299
+ Relative tolerance type, using a percentage.
300
+
301
+ Examples
302
+ --------
303
+ >>> from nextmv.cloud import MetricToleranceType
304
+ >>> tol_type = MetricToleranceType.absolute
305
+ >>> tol_type
306
+ <MetricToleranceType.absolute: 'absolute'>
307
+ """
308
+
245
309
  undefined = ""
246
310
  """Undefined tolerance type."""
247
311
  absolute = "absolute"
@@ -265,22 +329,22 @@ class MetricTolerance(BaseModel):
265
329
 
266
330
  Attributes
267
331
  ----------
268
- type : ToleranceType
332
+ type : MetricToleranceType
269
333
  Type of tolerance (absolute or relative).
270
334
  value : float
271
335
  Value of the tolerance.
272
336
 
273
337
  Examples
274
338
  --------
275
- >>> from nextmv.cloud import MetricTolerance, ToleranceType
276
- >>> tolerance = MetricTolerance(type=ToleranceType.absolute, value=0.1)
339
+ >>> from nextmv.cloud import MetricTolerance, MetricToleranceType
340
+ >>> tolerance = MetricTolerance(type=MetricToleranceType.absolute, value=0.1)
277
341
  >>> tolerance.type
278
- <ToleranceType.absolute: 'absolute'>
342
+ <MetricToleranceType.absolute: 'absolute'>
279
343
  >>> tolerance.value
280
344
  0.1
281
345
  """
282
346
 
283
- type: ToleranceType
347
+ type: MetricToleranceType
284
348
  """Type of tolerance."""
285
349
  value: float
286
350
  """Value of the tolerance."""
@@ -46,6 +46,7 @@ from nextmv.cloud.batch_experiment import (
46
46
  to_runs,
47
47
  )
48
48
  from nextmv.cloud.client import Client, get_size
49
+ from nextmv.cloud.ensemble import EnsembleDefinition, EvaluationRule, RunGroup
49
50
  from nextmv.cloud.input_set import InputSet, ManagedInput
50
51
  from nextmv.cloud.instance import Instance, InstanceConfiguration
51
52
  from nextmv.cloud.scenario import Scenario, ScenarioInputType, _option_sets, _scenarios_by_id
@@ -129,6 +130,8 @@ class Application:
129
130
  """Base endpoint for the application."""
130
131
  experiments_endpoint: str = "{base}/experiments"
131
132
  """Base endpoint for the experiments in the application."""
133
+ ensembles_endpoint: str = "{base}/ensembles"
134
+ """Base endpoint for managing the ensemble definitions in the application"""
132
135
 
133
136
  def __post_init__(self):
134
137
  """Initialize the endpoint and experiments_endpoint attributes.
@@ -138,6 +141,7 @@ class Application:
138
141
  """
139
142
  self.endpoint = self.endpoint.format(id=self.id)
140
143
  self.experiments_endpoint = self.experiments_endpoint.format(base=self.endpoint)
144
+ self.ensembles_endpoint = self.ensembles_endpoint.format(base=self.endpoint)
141
145
 
142
146
  @classmethod
143
147
  def new(
@@ -515,6 +519,30 @@ class Application:
515
519
  endpoint=f"{self.experiments_endpoint}/batch/{batch_id}",
516
520
  )
517
521
 
522
+ def delete_ensemble_definition(self, ensemble_definition_id: str) -> None:
523
+ """
524
+ Delete an ensemble definition.
525
+
526
+ Parameters
527
+ ----------
528
+ ensemble_definition_id : str
529
+ ID of the ensemble definition to delete.
530
+
531
+ Raises
532
+ ------
533
+ requests.HTTPError
534
+ If the response status code is not 2xx.
535
+
536
+ Examples
537
+ --------
538
+ >>> app.delete_ensemble_definition("development-ensemble-definition")
539
+ """
540
+
541
+ _ = self.client.request(
542
+ method="DELETE",
543
+ endpoint=f"{self.ensembles_endpoint}/{ensemble_definition_id}",
544
+ )
545
+
518
546
  def delete_scenario_test(self, scenario_test_id: str) -> None:
519
547
  """
520
548
  Delete a scenario test.
@@ -563,6 +591,39 @@ class Application:
563
591
  endpoint=f"{self.endpoint}/secrets/{secrets_collection_id}",
564
592
  )
565
593
 
594
+ def ensemble_definition(self, ensemble_definition_id: str) -> EnsembleDefinition:
595
+ """
596
+ Get an ensemble definition.
597
+
598
+ Parameters
599
+ ----------
600
+ ensemble_definition_id : str
601
+ ID of the ensemble definition to retrieve.
602
+
603
+ Returns
604
+ -------
605
+ EnsembleDefintion
606
+ The requested ensemble definition details.
607
+
608
+ Raises
609
+ ------
610
+ requests.HTTPError
611
+ If the response status code is not 2xx.
612
+
613
+ Examples
614
+ --------
615
+ >>> ensemble_definition = app.ensemble_definition("instance-123")
616
+ >>> print(ensemble_definition.name)
617
+ 'Production Ensemble Definition'
618
+ """
619
+
620
+ response = self.client.request(
621
+ method="GET",
622
+ endpoint=f"{self.ensembles_endpoint}/{ensemble_definition_id}",
623
+ )
624
+
625
+ return EnsembleDefinition.from_dict(response.json())
626
+
566
627
  @staticmethod
567
628
  def exists(client: Client, id: str) -> bool:
568
629
  """
@@ -748,6 +809,36 @@ class Application:
748
809
 
749
810
  return [BatchExperimentMetadata.from_dict(batch_experiment) for batch_experiment in response.json()]
750
811
 
812
+ def list_ensemble_definitions(self) -> list[EnsembleDefinition]:
813
+ """
814
+ List all ensemble_definitions.
815
+
816
+ Returns
817
+ -------
818
+ list[EnsembleDefinition]
819
+ List of all ensemble definitions associated with this application.
820
+
821
+ Raises
822
+ ------
823
+ requests.HTTPError
824
+ If the response status code is not 2xx.
825
+
826
+ Examples
827
+ --------
828
+ >>> ensemble_definitions = app.list_ensemble_definitions()
829
+ >>> for ensemble_definition in ensemble_definitions:
830
+ ... print(ensemble_definition.name)
831
+ 'Development Ensemble Definition'
832
+ 'Production Ensemble Definition'
833
+ """
834
+
835
+ response = self.client.request(
836
+ method="GET",
837
+ endpoint=f"{self.ensembles_endpoint}",
838
+ )
839
+
840
+ return [EnsembleDefinition.from_dict(ensemble_definition) for ensemble_definition in response.json()["items"]]
841
+
751
842
  def list_input_sets(self) -> list[InputSet]:
752
843
  """
753
844
  List all input sets.
@@ -1314,6 +1405,53 @@ class Application:
1314
1405
 
1315
1406
  return self.batch_experiment_with_polling(batch_id=batch_id, polling_options=polling_options)
1316
1407
 
1408
+ def new_ensemble_defintion(
1409
+ self,
1410
+ id: str,
1411
+ run_groups: list[RunGroup],
1412
+ rules: list[EvaluationRule],
1413
+ name: Optional[str] = None,
1414
+ description: Optional[str] = None,
1415
+ ) -> EnsembleDefinition:
1416
+ """
1417
+ Create a new ensemble definition.
1418
+
1419
+ Parameters
1420
+ ----------
1421
+ id: str
1422
+ ID of the ensemble defintion.
1423
+ run_groups: list[RunGroup]
1424
+ Information to facilitate the execution of child runs.
1425
+ rules: list[EvaluationRule]
1426
+ Information to facilitate the selection of
1427
+ a result for the ensemble run from child runs.
1428
+ name: Optional[str]
1429
+ Name of the ensemble definition.
1430
+ description: Optional[str]
1431
+ Description of the ensemble definition.
1432
+ """
1433
+
1434
+ if name is None:
1435
+ name = id
1436
+ if description is None:
1437
+ description = name
1438
+
1439
+ payload = {
1440
+ "id": id,
1441
+ "run_groups": [run_group.to_dict() for run_group in run_groups],
1442
+ "rules": [rule.to_dict() for rule in rules],
1443
+ "name": name,
1444
+ "description": description,
1445
+ }
1446
+
1447
+ response = self.client.request(
1448
+ method="POST",
1449
+ endpoint=f"{self.ensembles_endpoint}",
1450
+ payload=payload,
1451
+ )
1452
+
1453
+ return EnsembleDefinition.from_dict(response.json())
1454
+
1317
1455
  def new_input_set(
1318
1456
  self,
1319
1457
  id: str,
@@ -2236,13 +2374,14 @@ class Application:
2236
2374
 
2237
2375
  if id is None:
2238
2376
  id = safe_id(prefix="version")
2377
+ if name is None:
2378
+ name = id
2239
2379
 
2240
2380
  payload = {
2241
2381
  "id": id,
2382
+ "name": name,
2242
2383
  }
2243
2384
 
2244
- if name is not None:
2245
- payload["name"] = name
2246
2385
  if description is not None:
2247
2386
  payload["description"] = description
2248
2387
 
@@ -2934,6 +3073,98 @@ class Application:
2934
3073
  output_dir_path=output_dir_path,
2935
3074
  )
2936
3075
 
3076
+ def update_batch_experiment(
3077
+ self,
3078
+ batch_experiment_id: str,
3079
+ name: Optional[str] = None,
3080
+ description: Optional[str] = None,
3081
+ ) -> BatchExperimentInformation:
3082
+ """
3083
+ Update a batch experiment.
3084
+
3085
+ Parameters
3086
+ ----------
3087
+ batch_experiment_id : str
3088
+ ID of the batch experiment to update.
3089
+ name : Optional[str], default=None
3090
+ Optional name of the batch experiment.
3091
+ description : Optional[str], default=None
3092
+ Optional description of the batch experiment.
3093
+
3094
+ Returns
3095
+ -------
3096
+ BatchExperimentInformation
3097
+ The information with the updated batch experiment.
3098
+
3099
+ Raises
3100
+ ------
3101
+ requests.HTTPError
3102
+ If the response status code is not 2xx.
3103
+ """
3104
+
3105
+ payload = {}
3106
+
3107
+ if name is not None:
3108
+ payload["name"] = name
3109
+ if description is not None:
3110
+ payload["description"] = description
3111
+
3112
+ response = self.client.request(
3113
+ method="PATCH",
3114
+ endpoint=f"{self.experiments_endpoint}/batch/{batch_experiment_id}",
3115
+ payload=payload,
3116
+ )
3117
+
3118
+ return BatchExperimentInformation.from_dict(response.json())
3119
+
3120
+ def update_ensemble_definition(
3121
+ self,
3122
+ id: str,
3123
+ name: Optional[str] = None,
3124
+ description: Optional[str] = None,
3125
+ ) -> EnsembleDefinition:
3126
+ """
3127
+ Update an ensemble definition.
3128
+
3129
+ Parameters
3130
+ ----------
3131
+ id : str
3132
+ ID of the ensemble definition to update.
3133
+ name : Optional[str], default=None
3134
+ Optional name of the ensemble definition.
3135
+ description : Optional[str], default=None
3136
+ Optional description of the ensemble definition.
3137
+
3138
+ Returns
3139
+ -------
3140
+ EnsembleDefinition
3141
+ The updated ensemble definition.
3142
+
3143
+ Raises
3144
+ ------
3145
+ ValueError
3146
+ If neither name nor description is updated
3147
+ requests.HTTPError
3148
+ If the response status code is not 2xx.
3149
+ """
3150
+
3151
+ payload = {}
3152
+
3153
+ if name is None and description is None:
3154
+ raise ValueError("Must define at least one value among name and description to modify")
3155
+ if name is not None:
3156
+ payload["name"] = name
3157
+ if description is not None:
3158
+ payload["description"] = description
3159
+
3160
+ response = self.client.request(
3161
+ method="PATCH",
3162
+ endpoint=f"{self.ensembles_endpoint}/{id}",
3163
+ payload=payload,
3164
+ )
3165
+
3166
+ return EnsembleDefinition.from_dict(response.json())
3167
+
2937
3168
  def update_instance(
2938
3169
  self,
2939
3170
  id: str,
@@ -2997,50 +3228,6 @@ class Application:
2997
3228
 
2998
3229
  return Instance.from_dict(response.json())
2999
3230
 
3000
- def update_batch_experiment(
3001
- self,
3002
- batch_experiment_id: str,
3003
- name: Optional[str] = None,
3004
- description: Optional[str] = None,
3005
- ) -> BatchExperimentInformation:
3006
- """
3007
- Update a batch experiment.
3008
-
3009
- Parameters
3010
- ----------
3011
- batch_experiment_id : str
3012
- ID of the batch experiment to update.
3013
- name : Optional[str], default=None
3014
- Optional name of the batch experiment.
3015
- description : Optional[str], default=None
3016
- Optional description of the batch experiment.
3017
-
3018
- Returns
3019
- -------
3020
- BatchExperimentInformation
3021
- The information with the updated batch experiment.
3022
-
3023
- Raises
3024
- ------
3025
- requests.HTTPError
3026
- If the response status code is not 2xx.
3027
- """
3028
-
3029
- payload = {}
3030
-
3031
- if name is not None:
3032
- payload["name"] = name
3033
- if description is not None:
3034
- payload["description"] = description
3035
-
3036
- response = self.client.request(
3037
- method="PATCH",
3038
- endpoint=f"{self.experiments_endpoint}/batch/{batch_experiment_id}",
3039
- payload=payload,
3040
- )
3041
-
3042
- return BatchExperimentInformation.from_dict(response.json())
3043
-
3044
3231
  def update_managed_input(
3045
3232
  self,
3046
3233
  managed_input_id: str,
@@ -3681,65 +3868,40 @@ class Application:
3681
3868
  """
3682
3869
  Auxiliary function to validate the directory path and configuration.
3683
3870
  """
3871
+ input_type = self.__get_input_type(configuration)
3684
3872
 
3685
- if input_dir_path is None or input_dir_path == "":
3873
+ # If no explicit input type is defined, there is nothing to validate.
3874
+ if input_type is None:
3686
3875
  return
3687
3876
 
3688
- if configuration is None:
3689
- raise ValueError(
3690
- "If dir_path is provided, a RunConfiguration must also be provided.",
3691
- )
3692
-
3693
- config_format = self.__extract_config_format(configuration)
3694
-
3695
- if config_format is None:
3696
- raise ValueError(
3697
- "If dir_path is provided, RunConfiguration.format must also be provided.",
3698
- )
3699
-
3700
- input_type = self.__extract_input_type(config_format)
3701
-
3702
- if input_type is None or input_type in (InputFormat.JSON, InputFormat.TEXT):
3877
+ # Validate that the input directory path is provided when explicitly required.
3878
+ dir_types = (InputFormat.MULTI_FILE, InputFormat.CSV_ARCHIVE)
3879
+ if input_type in dir_types and not input_dir_path:
3703
3880
  raise ValueError(
3704
- "If dir_path is provided, RunConfiguration.format.format_input.input_type must be set to a valid type. "
3705
- f"Valid types are: {[InputFormat.CSV_ARCHIVE, InputFormat.MULTI_FILE]}",
3881
+ f"If RunConfiguration.format.format_input.input_type is set to {input_type}, "
3882
+ "then input_dir_path must be provided.",
3706
3883
  )
3707
3884
 
3708
- def __extract_config_format(self, configuration: Union[RunConfiguration, dict[str, Any]]) -> Any:
3709
- """Extract format from configuration, handling both RunConfiguration objects and dicts."""
3710
- if isinstance(configuration, RunConfiguration):
3711
- return configuration.format
3712
-
3713
- if isinstance(configuration, dict):
3714
- config_format = configuration.get("format")
3715
- if config_format is not None and isinstance(config_format, dict):
3716
- return Format.from_dict(config_format) if hasattr(Format, "from_dict") else config_format
3717
-
3718
- return config_format
3719
-
3720
- raise ValueError("Configuration must be a RunConfiguration object or a dict.")
3721
-
3722
- def __extract_input_type(self, config_format: Any) -> Any:
3723
- """Extract input type from config format."""
3724
- if isinstance(config_format, dict):
3725
- format_input = config_format.get("format_input") or config_format.get("input")
3726
- if format_input is None:
3727
- raise ValueError(
3728
- "If dir_path is provided, RunConfiguration.format.format_input must also be provided.",
3729
- )
3885
+ def __get_input_type(self, config: Union[RunConfiguration, dict[str, Any]]) -> Optional[InputFormat]:
3886
+ """
3887
+ Auxiliary function to extract the input type from the run configuration.
3888
+ """
3730
3889
 
3731
- if isinstance(format_input, dict):
3732
- return format_input.get("input_type") or format_input.get("type")
3890
+ if config is None:
3891
+ return None
3733
3892
 
3734
- return getattr(format_input, "input_type", None)
3893
+ if isinstance(config, dict):
3894
+ config = RunConfiguration.from_dict(config)
3735
3895
 
3736
- # Handle Format object
3737
- if config_format.format_input is None:
3738
- raise ValueError(
3739
- "If dir_path is provided, RunConfiguration.format.format_input must also be provided.",
3740
- )
3896
+ if (
3897
+ isinstance(config, RunConfiguration)
3898
+ and config.format is not None
3899
+ and config.format.format_input is not None
3900
+ and config.format.format_input.input_type is not None
3901
+ ):
3902
+ return config.format.format_input.input_type
3741
3903
 
3742
- return config_format.format_input.input_type
3904
+ return None
3743
3905
 
3744
3906
  def __package_inputs(self, dir_path: str) -> str:
3745
3907
  """