Skip to content
6 changes: 6 additions & 0 deletions nextmv/nextmv/cloud/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@
from .batch_experiment import ExperimentStatus as ExperimentStatus
from .client import Client as Client
from .client import get_size as get_size
from .ensemble import EnsembleDefinition as EnsembleDefinition
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strange, this should be ok.

from .ensemble import EvaluationRule as EvaluationRule
from .ensemble import RuleObjective as RuleObjective
from .ensemble import RuleTolerance as RuleTolerance
from .ensemble import RunGroup as RunGroup
from .ensemble import ToleranceType as EnsembleToleranceType

Check failure on line 71 in nextmv/nextmv/cloud/__init__.py

View workflow job for this annotation

GitHub Actions / python-lint (3.10, nextmv)

Ruff (F401)

nextmv/cloud/__init__.py:71:40: F401 `.ensemble.ToleranceType` imported but unused; consider removing, adding to `__all__`, or using a redundant alias

Check failure on line 71 in nextmv/nextmv/cloud/__init__.py

View workflow job for this annotation

GitHub Actions / python-lint (3.9, nextmv)

Ruff (F401)

nextmv/cloud/__init__.py:71:40: F401 `.ensemble.ToleranceType` imported but unused; consider removing, adding to `__all__`, or using a redundant alias

Check failure on line 71 in nextmv/nextmv/cloud/__init__.py

View workflow job for this annotation

GitHub Actions / python-lint (3.11, nextmv)

Ruff (F401)

nextmv/cloud/__init__.py:71:40: F401 `.ensemble.ToleranceType` imported but unused; consider removing, adding to `__all__`, or using a redundant alias

Check failure on line 71 in nextmv/nextmv/cloud/__init__.py

View workflow job for this annotation

GitHub Actions / python-lint (3.13, nextmv)

Ruff (F401)

nextmv/cloud/__init__.py:71:40: F401 `.ensemble.ToleranceType` imported but unused; consider removing, adding to `__all__`, or using a redundant alias

Check failure on line 71 in nextmv/nextmv/cloud/__init__.py

View workflow job for this annotation

GitHub Actions / python-lint (3.12, nextmv)

Ruff (F401)

nextmv/cloud/__init__.py:71:40: F401 `.ensemble.ToleranceType` imported but unused; consider removing, adding to `__all__`, or using a redundant alias
from .input_set import InputSet as InputSet
from .input_set import ManagedInput as ManagedInput
from .instance import Instance as Instance
Expand Down
192 changes: 190 additions & 2 deletions nextmv/nextmv/cloud/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,56 +22,61 @@
Function to poll for results with configurable options.
"""

import json
import os
import shutil
import tarfile
import tempfile
from dataclasses import dataclass
from datetime import datetime
from typing import Any, Optional, Union

import requests

from nextmv._serialization import deflated_serialize_json
from nextmv.base_model import BaseModel
from nextmv.cloud import package
from nextmv.cloud.acceptance_test import AcceptanceTest, Metric
from nextmv.cloud.batch_experiment import (
BatchExperiment,
BatchExperimentInformation,
BatchExperimentMetadata,
BatchExperimentRun,
ExperimentStatus,
to_runs,
)
from nextmv.cloud.client import Client, get_size
from nextmv.cloud.ensemble import (
EnsembleDefinition,
RunGroup,
EvaluationRule,
)
from nextmv.cloud.input_set import InputSet, ManagedInput
from nextmv.cloud.instance import Instance, InstanceConfiguration
from nextmv.cloud.scenario import Scenario, ScenarioInputType, _option_sets, _scenarios_by_id
from nextmv.cloud.secrets import Secret, SecretsCollection, SecretsCollectionSummary
from nextmv.cloud.url import DownloadURL, UploadURL
from nextmv.cloud.version import Version
from nextmv.input import Input, InputFormat
from nextmv.logger import log
from nextmv.manifest import Manifest
from nextmv.model import Model, ModelConfiguration
from nextmv.options import Options
from nextmv.output import Output, OutputFormat
from nextmv.polling import DEFAULT_POLLING_OPTIONS, PollingOptions, poll
from nextmv.run import (
ExternalRunResult,
Format,
FormatInput,
FormatOutput,
RunConfiguration,
RunInformation,
RunLog,
RunResult,
TrackedRun,
)
from nextmv.safe import safe_id, safe_name_and_id
from nextmv.status import StatusV2

Check failure on line 79 in nextmv/nextmv/cloud/application.py

View workflow job for this annotation

GitHub Actions / python-lint (3.10, nextmv)

Ruff (I001)

nextmv/cloud/application.py:25:1: I001 Import block is un-sorted or un-formatted

Check failure on line 79 in nextmv/nextmv/cloud/application.py

View workflow job for this annotation

GitHub Actions / python-lint (3.9, nextmv)

Ruff (I001)

nextmv/cloud/application.py:25:1: I001 Import block is un-sorted or un-formatted

Check failure on line 79 in nextmv/nextmv/cloud/application.py

View workflow job for this annotation

GitHub Actions / python-lint (3.11, nextmv)

Ruff (I001)

nextmv/cloud/application.py:25:1: I001 Import block is un-sorted or un-formatted

Check failure on line 79 in nextmv/nextmv/cloud/application.py

View workflow job for this annotation

GitHub Actions / python-lint (3.13, nextmv)

Ruff (I001)

nextmv/cloud/application.py:25:1: I001 Import block is un-sorted or un-formatted

Check failure on line 79 in nextmv/nextmv/cloud/application.py

View workflow job for this annotation

GitHub Actions / python-lint (3.12, nextmv)

Ruff (I001)

nextmv/cloud/application.py:25:1: I001 Import block is un-sorted or un-formatted

# Maximum size of the run input/output in bytes. This constant defines the
# maximum allowed size for run inputs and outputs. When the size exceeds this
Expand Down Expand Up @@ -551,6 +556,30 @@
endpoint=f"{self.endpoint}/secrets/{secrets_collection_id}",
)

def delete_ensemble_definition(self, ensemble_definition_id: str) -> None:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: we try to organize the methods in this class alphabetically, so this one should be above delete_scenario_test.

"""
Delete a secrets collection.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not accurate.


Parameters
----------
ensemble_definition_id : str
ID of the ensemble definition to delete.

Raises
------
requests.HTTPError
If the response status code is not 2xx.

Examples
--------
>>> app.delete_ensemble_definition("development-ensemble-definition")
"""

_ = self.client.request(
method="DELETE",
endpoint=f"{self.endpoint}/ensembles/{ensemble_definition_id}",
)

@staticmethod
def exists(client: Client, id: str) -> bool:
"""
Expand Down Expand Up @@ -683,6 +712,39 @@
return False
raise e

def ensemble_definition(self, ensemble_definition_id: str) -> EnsembleDefinition:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: same comment about ordering methods alphabetically inside the class.

"""
Get an ensemble definition.

Parameters
----------
ensemble_definition_id : str
ID of the ensemble definition to retrieve.

Returns
-------
EnsembleDefintion
The requested ensemble definition details.

Raises
------
requests.HTTPError
If the response status code is not 2xx.

Examples
--------
>>> ensemble_definition = app.ensemble_definition("instance-123")
>>> print(ensemble_definition.name)
'Production Ensemble Definition'
"""

response = self.client.request(
method="GET",
endpoint=f"{self.endpoint}/ensembles/{ensemble_definition_id}",
)

return EnsembleDefinition.from_dict(response.json())

def list_acceptance_tests(self) -> list[AcceptanceTest]:
"""
List all acceptance tests.
Expand Down Expand Up @@ -796,6 +858,36 @@

return [Instance.from_dict(instance) for instance in response.json()]

def list_ensemble_definitions(self) -> list[EnsembleDefinition]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: same comment here about ordering alphabetically.

"""
List all ensemble_definitions.

Returns
-------
list['EnsembleDefinition']
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
list['EnsembleDefinition']
list[EnsembleDefinition]

I think in this case backticks are not needed.

List of all ensemble definitions associated with this application.

Raises
------
requests.HTTPError
If the response status code is not 2xx.

Examples
--------
>>> ensemble_definitions = app.list_ensemble_definitions()
>>> for ensemble_definition in ensemble_definitions:
... print(ensemble_definition.name)
'Development Ensemble Definition'
'Production Ensemble Definition'
"""

response = self.client.request(
method="GET",
endpoint=f"{self.endpoint}/ensembles",
)

return [EnsembleDefinition.from_dict(ensemble_definition) for ensemble_definition in response.json()["items"]]

def list_managed_inputs(self) -> list[ManagedInput]:
"""
List all managed inputs.
Expand Down Expand Up @@ -1280,6 +1372,53 @@

return self.batch_experiment_with_polling(batch_id=batch_id, polling_options=polling_options)

def new_ensemble_defintion(
self,
id: str,
run_groups: list[RunGroup],
rules: list[EvaluationRule],
name: Optional[str] = None,
description: Optional[str] = None,
) -> EnsembleDefinition:
"""
Create a new ensemble definition.

Parameters
----------
id: str
ID of the ensemble defintion.
name: Optional[str]
Name of the ensemble definition.
description: Optional[str]
Description of the ensemble definition.
run_groups: Optional[list[RunGroup]]
Information to facilitate the execution of child runs.
rules: Optional[list[EvaluationRule]]
Information to facilitate the selection of
a result for the ensemble run from child runs.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Parameters should be in the same order they are expected in the function arguments.

"""

if name is None:
name = id
if description is None:
description = name

payload = {
"id": id,
"run_groups": [run_group.to_dict() for run_group in run_groups],
"rules": [rule.to_dict() for rule in rules],
"name": name,
"description": description,
}

response = self.client.request(
method="POST",
endpoint=f"{self.endpoint}/ensembles",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: consider defining ensembles as an attribute of the class, similar to experiments_endpoint:

experiments_endpoint: str = "{base}/experiments"

That way, you can use {self.ensembles_endpoint} as opposed to {self.endpoint}/ensembles here and in other places.

payload=payload,
)

return EnsembleDefinition.from_dict(response.json())

def new_input_set(
self,
id: str,
Expand Down Expand Up @@ -2202,13 +2341,14 @@

if id is None:
id = safe_id(prefix="version")
if name is None:
name = id

payload = {
"id": id,
"name": name,
}

if name is not None:
payload["name"] = name
if description is not None:
payload["description"] = description

Expand Down Expand Up @@ -2963,6 +3103,54 @@

return Instance.from_dict(response.json())

def update_ensemble_definition(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: same comment about alphabetical order.

self,
id: str,
name: Optional[str] = None,
description: Optional[str] = None,
) -> EnsembleDefinition:
"""
Update an ensemble definition.

Parameters
----------
id : str
ID of the ensemble definition to update.
name : Optional[str], default=None
Optional name of the ensemble definition.
description : Optional[str], default=None
Optional description of the ensemble definition.

Returns
-------
EnsembleDefinition
The updated ensemble definition.

Raises
------
ValueError
If neither name nor description is updated
requests.HTTPError
If the response status code is not 2xx.
"""

payload = {}

if name is None and description is None:
raise ValueError("Must define at least one value among name and description to modify")
if name is not None:
payload["name"] = name
if description is not None:
payload["description"] = description

response = self.client.request(
method="PATCH",
endpoint=f"{self.endpoint}/ensembles/{id}",
payload=payload,
)

return EnsembleDefinition.from_dict(response.json())

def update_batch_experiment(
self,
batch_experiment_id: str,
Expand Down
Loading
Loading