nextmv 0.32.0__py3-none-any.whl → 0.33.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/__init__.py +1 -0
- nextmv/cloud/__init__.py +7 -0
- nextmv/cloud/acceptance_test.py +69 -5
- nextmv/cloud/application.py +257 -95
- nextmv/cloud/ensemble.py +248 -0
- nextmv/cloud/package.py +62 -24
- nextmv/local/executor.py +2 -1
- nextmv/manifest.py +61 -2
- {nextmv-0.32.0.dist-info → nextmv-0.33.0.dist-info}/METADATA +1 -1
- {nextmv-0.32.0.dist-info → nextmv-0.33.0.dist-info}/RECORD +13 -12
- {nextmv-0.32.0.dist-info → nextmv-0.33.0.dist-info}/WHEEL +0 -0
- {nextmv-0.32.0.dist-info → nextmv-0.33.0.dist-info}/licenses/LICENSE +0 -0
nextmv/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "v0.
|
|
1
|
+
__version__ = "v0.33.0"
|
nextmv/__init__.py
CHANGED
|
@@ -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
|
nextmv/cloud/__init__.py
CHANGED
|
@@ -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
|
nextmv/cloud/acceptance_test.py
CHANGED
|
@@ -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 :
|
|
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,
|
|
276
|
-
>>> tolerance = MetricTolerance(type=
|
|
339
|
+
>>> from nextmv.cloud import MetricTolerance, MetricToleranceType
|
|
340
|
+
>>> tolerance = MetricTolerance(type=MetricToleranceType.absolute, value=0.1)
|
|
277
341
|
>>> tolerance.type
|
|
278
|
-
<
|
|
342
|
+
<MetricToleranceType.absolute: 'absolute'>
|
|
279
343
|
>>> tolerance.value
|
|
280
344
|
0.1
|
|
281
345
|
"""
|
|
282
346
|
|
|
283
|
-
type:
|
|
347
|
+
type: MetricToleranceType
|
|
284
348
|
"""Type of tolerance."""
|
|
285
349
|
value: float
|
|
286
350
|
"""Value of the tolerance."""
|
nextmv/cloud/application.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
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
|
|
3705
|
-
|
|
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
|
|
3709
|
-
"""
|
|
3710
|
-
|
|
3711
|
-
|
|
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
|
-
|
|
3732
|
-
|
|
3890
|
+
if config is None:
|
|
3891
|
+
return None
|
|
3733
3892
|
|
|
3734
|
-
|
|
3893
|
+
if isinstance(config, dict):
|
|
3894
|
+
config = RunConfiguration.from_dict(config)
|
|
3735
3895
|
|
|
3736
|
-
|
|
3737
|
-
|
|
3738
|
-
|
|
3739
|
-
|
|
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
|
|
3904
|
+
return None
|
|
3743
3905
|
|
|
3744
3906
|
def __package_inputs(self, dir_path: str) -> str:
|
|
3745
3907
|
"""
|
nextmv/cloud/ensemble.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Classes for working with Nextmv Cloud Ensemble Runs.
|
|
3
|
+
|
|
4
|
+
This module provides classes for interacting with ensemble runs in Nextmv Cloud.
|
|
5
|
+
It details the core data structures for ensemble definitions.
|
|
6
|
+
|
|
7
|
+
Classes
|
|
8
|
+
-------
|
|
9
|
+
RunGroup
|
|
10
|
+
A structure to group execution of child runs for an ensemble run.
|
|
11
|
+
RuleObjective
|
|
12
|
+
An enum that specifies the supported evaluation rule objectives.
|
|
13
|
+
ToleranceType
|
|
14
|
+
An enum that specifies the supported tolerance types for evaluation rules.
|
|
15
|
+
RuleTolerance
|
|
16
|
+
A structure for defining tolerance thresholds for an evaluation rule
|
|
17
|
+
EvaluationRule
|
|
18
|
+
A structure to evaluate run results for an ensemble run.
|
|
19
|
+
EnsembleDefinition
|
|
20
|
+
Representation of a Nextmv Cloud Ensemble Definition for an application.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from datetime import datetime
|
|
24
|
+
from enum import Enum
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
from nextmv.base_model import BaseModel
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RunGroup(BaseModel):
|
|
31
|
+
"""A structure to group child runs for an ensemble run.
|
|
32
|
+
|
|
33
|
+
You can import the `RunGroup` class directly from `cloud`:
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from nextmv.cloud import RunGroup
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
This class represents a grouping of child runs that share a configuration
|
|
40
|
+
for ensemble run executions.
|
|
41
|
+
|
|
42
|
+
Parameters
|
|
43
|
+
----------
|
|
44
|
+
id : str
|
|
45
|
+
The unique identifier of the run group.
|
|
46
|
+
instance_id : str
|
|
47
|
+
ID of the app instance that this run group executes on.
|
|
48
|
+
options : dict, optional
|
|
49
|
+
Runtime options/parameters for the application.
|
|
50
|
+
repetitions : int, optional
|
|
51
|
+
The number of times the run is to be repeated on the instance and with
|
|
52
|
+
the options defined in the run group
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
id: str
|
|
56
|
+
"""The unique identifier of the run group."""
|
|
57
|
+
instance_id: str
|
|
58
|
+
"""ID of the app instance that this run group executes on."""
|
|
59
|
+
options: Optional[dict] = None
|
|
60
|
+
"""Runtime options/parameters for the application."""
|
|
61
|
+
repetitions: Optional[int] = None
|
|
62
|
+
"""The number of times the run is to be repeated on the instance and with
|
|
63
|
+
the options defined in the run group"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class RuleObjective(str, Enum):
|
|
67
|
+
"""The value of this data determines how a value of a run is optimized to
|
|
68
|
+
determined which ensemble child run is the "best" for a given metric and
|
|
69
|
+
rule, as well as which other ones are within tolerance of that run for the
|
|
70
|
+
purposes of selecting a result for the ensemble run from among the child runs.
|
|
71
|
+
|
|
72
|
+
You can import the `RuleObjective` class directly from `cloud`:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from nextmv.cloud import RuleObjective
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
This enum specifies the supported evaluation rule objectives.
|
|
79
|
+
|
|
80
|
+
Attributes
|
|
81
|
+
----------
|
|
82
|
+
MAXIMIZE : str
|
|
83
|
+
Maximize the value of the evaluated metric.
|
|
84
|
+
MINIMIZE : str
|
|
85
|
+
Minimize the value of the evaluated metric.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
MAXIMIZE = "maximize"
|
|
89
|
+
"""Maximize the value of the evaluated metric."""
|
|
90
|
+
MINIMIZE = "minimize"
|
|
91
|
+
"""Minimize the value of the evaluated metric."""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class RuleToleranceType(str, Enum):
|
|
95
|
+
"""The type of comparison used to determine if a run metric is within
|
|
96
|
+
tolerance of a the "best" run for that rule and metric
|
|
97
|
+
|
|
98
|
+
You can import the `RuleToleranceType` class directly from `cloud`:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from nextmv.cloud import RuleToleranceType
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
This enum specifies the supported tolerance types.
|
|
105
|
+
|
|
106
|
+
Attributes
|
|
107
|
+
----------
|
|
108
|
+
ABSOLUTE : str
|
|
109
|
+
Uses the absolute difference between the value of the "best" run and
|
|
110
|
+
the run being evaluated for tolerance
|
|
111
|
+
RELATIVE : str
|
|
112
|
+
Uses the the percentage of the "best" run by which the run being
|
|
113
|
+
evaluted for tolerance differs. A value of `1` is 100%.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
ABSOLUTE = "absolute"
|
|
117
|
+
"""Uses the absolute difference between the value of the "best" run and
|
|
118
|
+
the run being evaluated for tolerance"""
|
|
119
|
+
RELATIVE = "relative"
|
|
120
|
+
"""Uses the the percentage of the "best" run by which the run being
|
|
121
|
+
evaluted for tolerance differs. A value of `1` is 100%."""
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class RuleTolerance(BaseModel):
|
|
125
|
+
"""A structure used to determine if a run is within tolerance of of the best
|
|
126
|
+
run (as determined by the objective of the `EvaluationRule` it is defined on).
|
|
127
|
+
|
|
128
|
+
You can import the `RuleTolerance` class directly from `cloud`:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
from nextmv.cloud import RuleTolerance
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
This class represents the tolerance on a particular evaluation rule by
|
|
135
|
+
which a child run may be selected as the result of an ensemble run.
|
|
136
|
+
|
|
137
|
+
value : float
|
|
138
|
+
The value within which runs can deviate from the "best" run
|
|
139
|
+
for that metric to be considered within tolerance of it.
|
|
140
|
+
type : ToleranceType
|
|
141
|
+
The method by which runs are determined to be within tolerance.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
value: float
|
|
145
|
+
"""The value within which runs can deviate from the "best" run
|
|
146
|
+
for that metric to be considered within tolerance of it."""
|
|
147
|
+
type: RuleToleranceType
|
|
148
|
+
"""The method by which runs are determined to be within tolerance."""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class EvaluationRule(BaseModel):
|
|
152
|
+
"""A structure to evaluate run results for an ensemble run.
|
|
153
|
+
|
|
154
|
+
You can import the `EvaluationRule` class directly from `cloud`:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from nextmv.cloud import EvaluationRule
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This class represents a rule by which the child runs for an ensemble run
|
|
161
|
+
will be evaluated for the purpose of selecting an optimal result for the
|
|
162
|
+
ensemble run.
|
|
163
|
+
|
|
164
|
+
Parameters
|
|
165
|
+
----------
|
|
166
|
+
id : str
|
|
167
|
+
The unique identifier of the evaluation rule.
|
|
168
|
+
statistics_path : str
|
|
169
|
+
The path within the statistics of a run output (conforming to Nextmv
|
|
170
|
+
statistics convention and flattened to a string starting with `$` and
|
|
171
|
+
delimited by `.` e.g. `$.result.value`.)
|
|
172
|
+
objective : RuleObjective
|
|
173
|
+
The objective by which runs are optimized for this rule
|
|
174
|
+
tolerance : RuleTolerance
|
|
175
|
+
The tolerance by which runs can be accepted as a potential result
|
|
176
|
+
for an evaluation rule
|
|
177
|
+
index : int, optional
|
|
178
|
+
The index (non-negative integer) of the evalutation rule. Lower indicies
|
|
179
|
+
are evaluated first.
|
|
180
|
+
"""
|
|
181
|
+
|
|
182
|
+
id: str
|
|
183
|
+
"""The unique identifier of the evaluation rule."""
|
|
184
|
+
statistics_path: str
|
|
185
|
+
"""The path within the statistics of a run output (conforming to Nextmv
|
|
186
|
+
statistics convention and flattened to a string starting with `$` and
|
|
187
|
+
delimited by `.` e.g. `$.result.value`.)"""
|
|
188
|
+
objective: RuleObjective
|
|
189
|
+
"""The objective by which runs are optimized for this rule"""
|
|
190
|
+
tolerance: RuleTolerance
|
|
191
|
+
"""The tolerance by which runs can be accepted as a potential result
|
|
192
|
+
for an evaluation rule"""
|
|
193
|
+
index: int
|
|
194
|
+
"""The index (non-negative integer) of the evalutation rule. Lower indicies
|
|
195
|
+
are evaluated first."""
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
class EnsembleDefinition(BaseModel):
|
|
199
|
+
"""An ensemble definition for an application.
|
|
200
|
+
|
|
201
|
+
You can import the `EnsembleDefinition` class directly from `cloud`:
|
|
202
|
+
|
|
203
|
+
```python
|
|
204
|
+
from nextmv.cloud import EnsembleDefinition
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
A Nextmv Cloud ensemble definition represents a structure by which an
|
|
208
|
+
application can coordinate and execute, and determine the optimal result of
|
|
209
|
+
an ensemble run.
|
|
210
|
+
|
|
211
|
+
Parameters
|
|
212
|
+
----------
|
|
213
|
+
id : str
|
|
214
|
+
The unique identifier of the ensemble definition.
|
|
215
|
+
application_id : str
|
|
216
|
+
ID of the application that this ensemble definition belongs to.
|
|
217
|
+
name : str
|
|
218
|
+
Human-readable name of the ensemble definition.
|
|
219
|
+
description : str
|
|
220
|
+
Detailed description of the ensemble definition.
|
|
221
|
+
run_groups : list[RunGroup], optional
|
|
222
|
+
The run groups that structure the execution of an ensemble run
|
|
223
|
+
rules : list[EvaluationRule], optional
|
|
224
|
+
The rules by which ensemble child runs are evaluated
|
|
225
|
+
to find an optimal result.
|
|
226
|
+
created_at : datetime
|
|
227
|
+
Timestamp when the ensemble definition was created.
|
|
228
|
+
updated_at : datetime
|
|
229
|
+
Timestamp when the ensemble definition was last updated.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
id: str
|
|
233
|
+
"""The unique identifier of the ensemble definition."""
|
|
234
|
+
application_id: str
|
|
235
|
+
"""ID of the application that this ensemble definition belongs to."""
|
|
236
|
+
name: str = ""
|
|
237
|
+
"""Human-readable name of the ensemble definition."""
|
|
238
|
+
description: str = ""
|
|
239
|
+
"""Detailed description of the ensemble definition."""
|
|
240
|
+
run_groups: list[RunGroup]
|
|
241
|
+
"""The run groups that structure the execution of an ensemble run"""
|
|
242
|
+
rules: list[EvaluationRule]
|
|
243
|
+
"""The rules by which ensemble child runs are evaluated
|
|
244
|
+
to find an optimal result."""
|
|
245
|
+
created_at: datetime
|
|
246
|
+
"""Timestamp when the ensemble definition was created."""
|
|
247
|
+
updated_at: datetime
|
|
248
|
+
"""Timestamp when the ensemble definition was last updated."""
|
nextmv/cloud/package.py
CHANGED
|
@@ -222,7 +222,7 @@ def __handle_python(
|
|
|
222
222
|
__install_dependencies(manifest, app_dir, temp_dir)
|
|
223
223
|
|
|
224
224
|
|
|
225
|
-
def __install_dependencies(
|
|
225
|
+
def __install_dependencies( # noqa: C901 # complexity
|
|
226
226
|
manifest: Manifest,
|
|
227
227
|
app_dir: str,
|
|
228
228
|
temp_dir: str,
|
|
@@ -253,31 +253,58 @@ def __install_dependencies(
|
|
|
253
253
|
if not os.path.isfile(os.path.join(app_dir, pip_requirements)):
|
|
254
254
|
raise FileNotFoundError(f"pip requirements file '{pip_requirements}' not found in '{app_dir}'")
|
|
255
255
|
|
|
256
|
+
platform_filter = []
|
|
257
|
+
if not manifest.python.arch or manifest.python.arch == "arm64":
|
|
258
|
+
platform_filter.extend(
|
|
259
|
+
[
|
|
260
|
+
"--platform=manylinux2014_aarch64",
|
|
261
|
+
"--platform=manylinux_2_17_aarch64",
|
|
262
|
+
"--platform=manylinux_2_24_aarch64",
|
|
263
|
+
"--platform=manylinux_2_28_aarch64",
|
|
264
|
+
"--platform=linux_aarch64",
|
|
265
|
+
]
|
|
266
|
+
)
|
|
267
|
+
elif manifest.python.arch == "amd64":
|
|
268
|
+
platform_filter.extend(
|
|
269
|
+
[
|
|
270
|
+
"--platform=manylinux2014_x86_64",
|
|
271
|
+
"--platform=manylinux_2_17_x86_64",
|
|
272
|
+
"--platform=manylinux_2_24_x86_64",
|
|
273
|
+
"--platform=manylinux_2_28_x86_64",
|
|
274
|
+
"--platform=linux_x86_64",
|
|
275
|
+
]
|
|
276
|
+
)
|
|
277
|
+
else:
|
|
278
|
+
raise Exception(f"unknown architecture '{manifest.python.arch}' specified in manifest")
|
|
279
|
+
|
|
280
|
+
version_filter = ["--python-version=3.11"]
|
|
281
|
+
if manifest.python.version:
|
|
282
|
+
__confirm_python_bundling_version(manifest.python.version)
|
|
283
|
+
version_filter = [f"--python-version={manifest.python.version}"]
|
|
284
|
+
|
|
256
285
|
py_cmd = __get_python_command()
|
|
257
286
|
dep_dir = os.path.join(".nextmv", "python", "deps")
|
|
258
|
-
command =
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
"--quiet",
|
|
280
|
-
]
|
|
287
|
+
command = (
|
|
288
|
+
[
|
|
289
|
+
py_cmd,
|
|
290
|
+
"-m",
|
|
291
|
+
"pip",
|
|
292
|
+
"install",
|
|
293
|
+
"-r",
|
|
294
|
+
pip_requirements,
|
|
295
|
+
"--only-binary=:all:",
|
|
296
|
+
"--implementation=cp",
|
|
297
|
+
"--upgrade",
|
|
298
|
+
"--no-warn-conflicts",
|
|
299
|
+
"--target",
|
|
300
|
+
os.path.join(temp_dir, dep_dir),
|
|
301
|
+
"--no-user", # We explicitly avoid user mode (mainly to fix issues with Windows store Python installations)
|
|
302
|
+
"--no-input",
|
|
303
|
+
"--quiet",
|
|
304
|
+
]
|
|
305
|
+
+ platform_filter
|
|
306
|
+
+ version_filter
|
|
307
|
+
)
|
|
281
308
|
result = subprocess.run(
|
|
282
309
|
command,
|
|
283
310
|
cwd=app_dir,
|
|
@@ -381,6 +408,17 @@ def __confirm_python_version(output: str) -> None:
|
|
|
381
408
|
raise Exception("python version 3.9 or higher is required")
|
|
382
409
|
|
|
383
410
|
|
|
411
|
+
def __confirm_python_bundling_version(version: str) -> None:
|
|
412
|
+
# Only accept versions in the form "major.minor" where both are integers
|
|
413
|
+
re_version = re.compile(r"^(\d+)\.(\d+)$")
|
|
414
|
+
match = re_version.fullmatch(version)
|
|
415
|
+
if match:
|
|
416
|
+
major, minor = int(match.group(1)), int(match.group(2))
|
|
417
|
+
if major == 3 and minor >= 9:
|
|
418
|
+
return
|
|
419
|
+
raise Exception(f"python version 3.9 or higher is required for bundling, got {version}")
|
|
420
|
+
|
|
421
|
+
|
|
384
422
|
def __compress_tar(source: str, target: str) -> tuple[str, int]:
|
|
385
423
|
"""Compress the source directory into a tar.gz file in the target"""
|
|
386
424
|
|
nextmv/local/executor.py
CHANGED
|
@@ -32,6 +32,7 @@ import json
|
|
|
32
32
|
import os
|
|
33
33
|
import shutil
|
|
34
34
|
import subprocess
|
|
35
|
+
import sys
|
|
35
36
|
import tempfile
|
|
36
37
|
from datetime import datetime, timezone
|
|
37
38
|
from typing import Any, Optional, Union
|
|
@@ -143,7 +144,7 @@ def execute_run(
|
|
|
143
144
|
# supporting a Python-first experience, so we are not summoning
|
|
144
145
|
# applications that are not Python-based.
|
|
145
146
|
entrypoint = os.path.join(temp_src, manifest.entrypoint)
|
|
146
|
-
args = [
|
|
147
|
+
args = [sys.executable, entrypoint] + options_args(options)
|
|
147
148
|
|
|
148
149
|
result = subprocess.run(
|
|
149
150
|
args,
|
nextmv/manifest.py
CHANGED
|
@@ -49,7 +49,7 @@ from enum import Enum
|
|
|
49
49
|
from typing import Any, Optional, Union
|
|
50
50
|
|
|
51
51
|
import yaml
|
|
52
|
-
from pydantic import AliasChoices, Field
|
|
52
|
+
from pydantic import AliasChoices, Field, field_validator
|
|
53
53
|
|
|
54
54
|
from nextmv.base_model import BaseModel
|
|
55
55
|
from nextmv.input import InputFormat
|
|
@@ -166,6 +166,43 @@ class ManifestRuntime(str, Enum):
|
|
|
166
166
|
Based on the python runtime, it provisions (pre-installs) the Hexaly solver
|
|
167
167
|
to run Python applications.
|
|
168
168
|
"""
|
|
169
|
+
CUOPT = "ghcr.io/nextmv-io/runtime/cuopt:latest"
|
|
170
|
+
"""
|
|
171
|
+
A runtime providing the NVIDIA cuOpt solver.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class ManifestPythonArch(str, Enum):
|
|
176
|
+
"""
|
|
177
|
+
Target architecture for bundling Python apps.
|
|
178
|
+
|
|
179
|
+
You can import the `ManifestPythonArch` class directly from `nextmv`:
|
|
180
|
+
|
|
181
|
+
```python
|
|
182
|
+
from nextmv import ManifestPythonArch
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Attributes
|
|
186
|
+
----------
|
|
187
|
+
ARM64 : str
|
|
188
|
+
ARM 64-bit architecture.
|
|
189
|
+
AMD64 : str
|
|
190
|
+
AMD 64-bit architecture.
|
|
191
|
+
|
|
192
|
+
Examples
|
|
193
|
+
--------
|
|
194
|
+
>>> from nextmv import ManifestPythonArch
|
|
195
|
+
>>> arch = ManifestPythonArch.ARM64
|
|
196
|
+
>>> arch
|
|
197
|
+
<ManifestPythonArch.ARM64: 'arm64'>
|
|
198
|
+
>>> str(arch)
|
|
199
|
+
'arm64'
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
ARM64 = "arm64"
|
|
203
|
+
"""ARM 64-bit architecture."""
|
|
204
|
+
AMD64 = "amd64"
|
|
205
|
+
"""AMD 64-bit architecture."""
|
|
169
206
|
|
|
170
207
|
|
|
171
208
|
class ManifestBuild(BaseModel):
|
|
@@ -325,6 +362,12 @@ class ManifestPython(BaseModel):
|
|
|
325
362
|
Contains (additional) Python dependencies that will be bundled with the
|
|
326
363
|
app.
|
|
327
364
|
"""
|
|
365
|
+
arch: Optional[ManifestPythonArch] = None
|
|
366
|
+
"""The architecture this model is meant to run on. One of "arm64" or "amd64". Uses
|
|
367
|
+
"arm64" if not specified."""
|
|
368
|
+
version: Optional[Union[str, float]] = None
|
|
369
|
+
"""The Python version this model is meant to run with. Uses "3.11" if not specified.
|
|
370
|
+
"""
|
|
328
371
|
model: Optional[ManifestPythonModel] = None
|
|
329
372
|
"""Information about an encoded decision model.
|
|
330
373
|
|
|
@@ -332,6 +375,17 @@ class ManifestPython(BaseModel):
|
|
|
332
375
|
from the app bundle.
|
|
333
376
|
"""
|
|
334
377
|
|
|
378
|
+
@field_validator("version", mode="before")
|
|
379
|
+
@classmethod
|
|
380
|
+
def validate_version(cls, v: Optional[Union[str, float]]) -> Optional[str]:
|
|
381
|
+
# We allow the version to be a float in the manifest for convenience, but we want
|
|
382
|
+
# to store it as a string internally.
|
|
383
|
+
if v is None:
|
|
384
|
+
return None
|
|
385
|
+
if isinstance(v, float):
|
|
386
|
+
return str(v)
|
|
387
|
+
return v
|
|
388
|
+
|
|
335
389
|
|
|
336
390
|
class ManifestOptionUI(BaseModel):
|
|
337
391
|
"""
|
|
@@ -1025,7 +1079,12 @@ class Manifest(BaseModel):
|
|
|
1025
1079
|
|
|
1026
1080
|
def model_post_init(self, __context) -> None:
|
|
1027
1081
|
if self.entrypoint is None:
|
|
1028
|
-
if self.runtime in (
|
|
1082
|
+
if self.runtime in (
|
|
1083
|
+
ManifestRuntime.PYTHON,
|
|
1084
|
+
ManifestRuntime.HEXALY,
|
|
1085
|
+
ManifestRuntime.PYOMO,
|
|
1086
|
+
ManifestRuntime.CUOPT,
|
|
1087
|
+
):
|
|
1029
1088
|
self.entrypoint = "./main.py"
|
|
1030
1089
|
elif self.runtime == ManifestRuntime.DEFAULT:
|
|
1031
1090
|
self.entrypoint = "./main"
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
nextmv/__about__.py,sha256=
|
|
1
|
+
nextmv/__about__.py,sha256=VyKoqs7qvl1UtyON_wE484wOoQ1-D2LWfsRhOrha9gg,24
|
|
2
2
|
nextmv/__entrypoint__.py,sha256=dA0iwwHtrq6Z9w9FxmxKLoBGLyhe7jWtUAU-Y3PEgHg,1094
|
|
3
|
-
nextmv/__init__.py,sha256=
|
|
3
|
+
nextmv/__init__.py,sha256=uM80mRRs2Ht2dP60DvxR11HmunFJ7EL7XUJHtKQi2qI,3683
|
|
4
4
|
nextmv/_serialization.py,sha256=JlSl6BL0M2Esf7F89GsGIZ__Pp8RnFRNM0UxYhuuYU4,2853
|
|
5
5
|
nextmv/base_model.py,sha256=qmJ4AsYr9Yv01HQX_BERrn3229gyoZrYyP9tcyqNfeU,2311
|
|
6
6
|
nextmv/deprecated.py,sha256=kEVfyQ-nT0v2ePXTNldjQG9uH5IlfQVy3L4tztIxwmU,1638
|
|
7
7
|
nextmv/input.py,sha256=m9sVfO9ZL3F5i1l8amEtlWlbkekyUP4C3y9DduHWGFs,40211
|
|
8
8
|
nextmv/logger.py,sha256=kNIbu46MisrzYe4T0hNMpWfRTKKacDVvbtQcNys_c_E,2513
|
|
9
|
-
nextmv/manifest.py,sha256=
|
|
9
|
+
nextmv/manifest.py,sha256=jCu5RUl6bip4kTDh4zaD4PTYNOIGHWKm34kfXD_rpzw,45898
|
|
10
10
|
nextmv/model.py,sha256=vI3pSV3iTwjRPflar7nAg-6h98XRUyi9II5O2J06-Kc,15018
|
|
11
11
|
nextmv/options.py,sha256=yPJu5lYMbV6YioMwAXv7ctpZUggLXKlZc9CqIbUFvE4,37895
|
|
12
12
|
nextmv/output.py,sha256=HdvWYG3gIzwoXquulaEVI4LLchXJDjkbag0BkBPM0vQ,55128
|
|
@@ -14,15 +14,16 @@ nextmv/polling.py,sha256=nfefvWI1smm-lIzaXE-4DMlojp6KXIvVi88XLJYUmo8,9724
|
|
|
14
14
|
nextmv/run.py,sha256=lAWWkLml1C7wkXSvN8orBjudoz72Qv5ZCHCWY9e7xaY,45813
|
|
15
15
|
nextmv/safe.py,sha256=VAK4fGEurbLNji4Pg5Okga5XQSbI4aI9JJf95_68Z20,3867
|
|
16
16
|
nextmv/status.py,sha256=SCDLhh2om3yeO5FxO0x-_RShQsZNXEpjHNdCGdb3VUI,2787
|
|
17
|
-
nextmv/cloud/__init__.py,sha256=
|
|
18
|
-
nextmv/cloud/acceptance_test.py,sha256=
|
|
17
|
+
nextmv/cloud/__init__.py,sha256=2wI72lhWq81BYv1OpS0OOTT5-3sivpX0H4z5ANPoLMc,5051
|
|
18
|
+
nextmv/cloud/acceptance_test.py,sha256=ZEzCMrfJF-nUFr1nEr4IDgcoyavPhnanjFuPBJ79tAk,27731
|
|
19
19
|
nextmv/cloud/account.py,sha256=jIdGNyI3l3dVh2PuriAwAOrEuWRM150WgzxcBMVBNRw,6058
|
|
20
|
-
nextmv/cloud/application.py,sha256=
|
|
20
|
+
nextmv/cloud/application.py,sha256=zbirm_bbihnzhKsO8gAO_dJRAJV4FtXsQsSaismgt-I,139298
|
|
21
21
|
nextmv/cloud/batch_experiment.py,sha256=13ciRpgBabMMTyazfdfEAymD3rTPrTAAorECsANxxuA,10397
|
|
22
22
|
nextmv/cloud/client.py,sha256=E0DiUb377jvEnpXlRnfT1PGCI0Jm0lTUoX5VqeU91lk,18165
|
|
23
|
+
nextmv/cloud/ensemble.py,sha256=glrRgyRFcEH12fNUhEl1FOo6xOTDEaF478dxfX0wj2Y,8604
|
|
23
24
|
nextmv/cloud/input_set.py,sha256=NkzA6_hwgD-YwoirzwvZrObIoBTfurry7Os3jo4DyXc,4236
|
|
24
25
|
nextmv/cloud/instance.py,sha256=SS4tbp0LQMWDaeYpwcNxJei82oi_Hozv1t5i3QGjASY,4024
|
|
25
|
-
nextmv/cloud/package.py,sha256=
|
|
26
|
+
nextmv/cloud/package.py,sha256=Xmt-daAeN9QJKpquV28IiZa2eYCX0P3wSS564JHvrtY,14495
|
|
26
27
|
nextmv/cloud/scenario.py,sha256=JRFTDiFBcrgud6wE2qDHUu5oO-Ur3zbPYhhB6ONCxTo,14263
|
|
27
28
|
nextmv/cloud/secrets.py,sha256=fA5cX0jfTsPVZWV7433wzETGlXpWRLHGswuObx9e6FQ,6820
|
|
28
29
|
nextmv/cloud/url.py,sha256=Fz70ywkWdCLmP21ZBmJwZi5kDbjpmsX_VlwVF_xQeHg,1836
|
|
@@ -38,12 +39,12 @@ nextmv/default_app/src/main.py,sha256=WWeN_xl_mcPhICl3rSCvdEjRkFXGmAnej88FhS-fAm
|
|
|
38
39
|
nextmv/default_app/src/visuals.py,sha256=WYK_YBnLmYo3TpVev1CpoNCuW5R7hk9QIkeCmvMn1Fs,1014
|
|
39
40
|
nextmv/local/__init__.py,sha256=6BsoqlK4dw6X11_uKzz9gBPfxKpdiol2FYO8R3X73SE,116
|
|
40
41
|
nextmv/local/application.py,sha256=qq14ihKxymg5NHqhU56qTu6lVmMYWLGYUJC4MrhWB68,45815
|
|
41
|
-
nextmv/local/executor.py,sha256=
|
|
42
|
+
nextmv/local/executor.py,sha256=ohAUrIRohcH_qGglK1hSFR0W6bPBSuMAcMwVkW5G4vM,24338
|
|
42
43
|
nextmv/local/geojson_handler.py,sha256=7FavJdkUonop-yskjis0x3qFGB8A5wZyoBUblw-bVhw,12540
|
|
43
44
|
nextmv/local/local.py,sha256=wUHuoAXqJIZpTJBh056hmQuiPEjlZvLTR2BC6Ino-WI,2619
|
|
44
45
|
nextmv/local/plotly_handler.py,sha256=bLb50e3AkVr_W-F6S7lXfeRdN60mG2jk3UElNmhoMWU,1930
|
|
45
46
|
nextmv/local/runner.py,sha256=hwkITHrQG_J9TzxufnaP1mjLWG-iSsNQD66UFZY4pp4,8602
|
|
46
|
-
nextmv-0.
|
|
47
|
-
nextmv-0.
|
|
48
|
-
nextmv-0.
|
|
49
|
-
nextmv-0.
|
|
47
|
+
nextmv-0.33.0.dist-info/METADATA,sha256=JRt0lALKQ1i0E_J2AhgozeUtC7kv_YGHsqK_-VDePsw,16008
|
|
48
|
+
nextmv-0.33.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
49
|
+
nextmv-0.33.0.dist-info/licenses/LICENSE,sha256=ZIbK-sSWA-OZprjNbmJAglYRtl5_K4l9UwAV3PGJAPc,11349
|
|
50
|
+
nextmv-0.33.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|