nextmv 0.26.0__py3-none-any.whl → 0.26.1__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/cloud/__init__.py +1 -0
- nextmv/cloud/application.py +85 -12
- nextmv/cloud/run.py +34 -2
- nextmv/logger.py +9 -2
- {nextmv-0.26.0.dist-info → nextmv-0.26.1.dist-info}/METADATA +1 -1
- {nextmv-0.26.0.dist-info → nextmv-0.26.1.dist-info}/RECORD +9 -9
- {nextmv-0.26.0.dist-info → nextmv-0.26.1.dist-info}/WHEEL +0 -0
- {nextmv-0.26.0.dist-info → nextmv-0.26.1.dist-info}/licenses/LICENSE +0 -0
nextmv/__about__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "v0.26.
|
|
1
|
+
__version__ = "v0.26.1"
|
nextmv/cloud/__init__.py
CHANGED
|
@@ -53,6 +53,7 @@ from .run import RunType as RunType
|
|
|
53
53
|
from .run import RunTypeConfiguration as RunTypeConfiguration
|
|
54
54
|
from .run import TrackedRun as TrackedRun
|
|
55
55
|
from .run import TrackedRunStatus as TrackedRunStatus
|
|
56
|
+
from .run import run_duration as run_duration
|
|
56
57
|
from .scenario import Scenario as Scenario
|
|
57
58
|
from .scenario import ScenarioConfiguration as ScenarioConfiguration
|
|
58
59
|
from .scenario import ScenarioInput as ScenarioInput
|
nextmv/cloud/application.py
CHANGED
|
@@ -317,18 +317,7 @@ class Application:
|
|
|
317
317
|
# If the request was successful, the application exists.
|
|
318
318
|
return True
|
|
319
319
|
except requests.HTTPError as e:
|
|
320
|
-
if (
|
|
321
|
-
# Check whether the error is caused by a 404 status code - meaning the app does not exist.
|
|
322
|
-
(hasattr(e, "response") and hasattr(e.response, "status_code") and e.response.status_code == 404)
|
|
323
|
-
or
|
|
324
|
-
# Check a possibly nested exception as well.
|
|
325
|
-
(
|
|
326
|
-
hasattr(e, "__cause__")
|
|
327
|
-
and hasattr(e.__cause__, "response")
|
|
328
|
-
and hasattr(e.__cause__.response, "status_code")
|
|
329
|
-
and e.__cause__.response.status_code == 404
|
|
330
|
-
)
|
|
331
|
-
):
|
|
320
|
+
if _is_not_exist_error(e):
|
|
332
321
|
return False
|
|
333
322
|
# Re-throw the exception if it is not the expected 404 error.
|
|
334
323
|
raise e from None
|
|
@@ -375,6 +364,25 @@ class Application:
|
|
|
375
364
|
|
|
376
365
|
return Instance.from_dict(response.json())
|
|
377
366
|
|
|
367
|
+
def instance_exists(self, instance_id: str) -> bool:
|
|
368
|
+
"""
|
|
369
|
+
Check if an instance exists.
|
|
370
|
+
|
|
371
|
+
Args:
|
|
372
|
+
instance_id: ID of the instance.
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
True if the instance exists, False otherwise.
|
|
376
|
+
"""
|
|
377
|
+
|
|
378
|
+
try:
|
|
379
|
+
self.instance(instance_id=instance_id)
|
|
380
|
+
return True
|
|
381
|
+
except requests.HTTPError as e:
|
|
382
|
+
if _is_not_exist_error(e):
|
|
383
|
+
return False
|
|
384
|
+
raise e
|
|
385
|
+
|
|
378
386
|
def list_acceptance_tests(self) -> list[AcceptanceTest]:
|
|
379
387
|
"""
|
|
380
388
|
List all acceptance tests.
|
|
@@ -581,6 +589,8 @@ class Application:
|
|
|
581
589
|
id: ID of the application. Will be generated if not provided.
|
|
582
590
|
description: Description of the application.
|
|
583
591
|
is_workflow: Whether the application is a Decision Workflow.
|
|
592
|
+
exist_ok: If True and an application with the same ID already exists,
|
|
593
|
+
return the existing application instead of creating a new one.
|
|
584
594
|
|
|
585
595
|
Returns:
|
|
586
596
|
The new application.
|
|
@@ -932,6 +942,7 @@ class Application:
|
|
|
932
942
|
name: str,
|
|
933
943
|
description: Optional[str] = None,
|
|
934
944
|
configuration: Optional[InstanceConfiguration] = None,
|
|
945
|
+
exist_ok: bool = False,
|
|
935
946
|
) -> Instance:
|
|
936
947
|
"""
|
|
937
948
|
Create a new instance and associate it with a version.
|
|
@@ -942,6 +953,8 @@ class Application:
|
|
|
942
953
|
name: Name of the instance. Will be generated if not provided.
|
|
943
954
|
description: Description of the instance. Will be generated if not provided.
|
|
944
955
|
configuration: Configuration to use for the instance.
|
|
956
|
+
exist_ok: If True and an instance with the same ID already exists,
|
|
957
|
+
return the existing instance instead of creating a new one.
|
|
945
958
|
|
|
946
959
|
Returns:
|
|
947
960
|
Instance.
|
|
@@ -950,6 +963,12 @@ class Application:
|
|
|
950
963
|
requests.HTTPError: If the response status code is not 2xx.
|
|
951
964
|
"""
|
|
952
965
|
|
|
966
|
+
if exist_ok and id is None:
|
|
967
|
+
raise ValueError("If exist_ok is True, id must be provided")
|
|
968
|
+
|
|
969
|
+
if exist_ok and self.instance_exists(instance_id=id):
|
|
970
|
+
return self.instance(instance_id=id)
|
|
971
|
+
|
|
953
972
|
payload = {
|
|
954
973
|
"version_id": version_id,
|
|
955
974
|
}
|
|
@@ -1478,6 +1497,7 @@ class Application:
|
|
|
1478
1497
|
id: Optional[str] = None,
|
|
1479
1498
|
name: Optional[str] = None,
|
|
1480
1499
|
description: Optional[str] = None,
|
|
1500
|
+
exist_ok: bool = False,
|
|
1481
1501
|
) -> Version:
|
|
1482
1502
|
"""
|
|
1483
1503
|
Create a new version using the current dev binary.
|
|
@@ -1486,6 +1506,8 @@ class Application:
|
|
|
1486
1506
|
id: ID of the version. Will be generated if not provided.
|
|
1487
1507
|
name: Name of the version. Will be generated if not provided.
|
|
1488
1508
|
description: Description of the version. Will be generated if not provided.
|
|
1509
|
+
exist_ok: If True and a version with the same ID already exists,
|
|
1510
|
+
return the existing version instead of creating a new one.
|
|
1489
1511
|
|
|
1490
1512
|
Returns:
|
|
1491
1513
|
Version.
|
|
@@ -1494,6 +1516,12 @@ class Application:
|
|
|
1494
1516
|
requests.HTTPError: If the response status code is not 2xx.
|
|
1495
1517
|
"""
|
|
1496
1518
|
|
|
1519
|
+
if exist_ok and id is None:
|
|
1520
|
+
raise ValueError("If exist_ok is True, id must be provided")
|
|
1521
|
+
|
|
1522
|
+
if exist_ok and self.version_exists(version_id=id):
|
|
1523
|
+
return self.version(version_id=id)
|
|
1524
|
+
|
|
1497
1525
|
payload = {}
|
|
1498
1526
|
|
|
1499
1527
|
if id is not None:
|
|
@@ -2201,6 +2229,25 @@ class Application:
|
|
|
2201
2229
|
|
|
2202
2230
|
return Version.from_dict(response.json())
|
|
2203
2231
|
|
|
2232
|
+
def version_exists(self, version_id: str) -> bool:
|
|
2233
|
+
"""
|
|
2234
|
+
Check if a version exists.
|
|
2235
|
+
|
|
2236
|
+
Args:
|
|
2237
|
+
version_id: ID of the version.
|
|
2238
|
+
|
|
2239
|
+
Returns:
|
|
2240
|
+
bool: True if the version exists, False otherwise.
|
|
2241
|
+
"""
|
|
2242
|
+
|
|
2243
|
+
try:
|
|
2244
|
+
self.version(version_id=version_id)
|
|
2245
|
+
return True
|
|
2246
|
+
except requests.HTTPError as e:
|
|
2247
|
+
if _is_not_exist_error(e):
|
|
2248
|
+
return False
|
|
2249
|
+
raise e
|
|
2250
|
+
|
|
2204
2251
|
def __run_result(
|
|
2205
2252
|
self,
|
|
2206
2253
|
run_id: str,
|
|
@@ -2436,3 +2483,29 @@ def poll(polling_options: PollingOptions, polling_func: Callable[[], tuple[any,
|
|
|
2436
2483
|
raise RuntimeError(
|
|
2437
2484
|
f"polling did not succeed after {polling_options.max_tries} tries",
|
|
2438
2485
|
)
|
|
2486
|
+
|
|
2487
|
+
|
|
2488
|
+
def _is_not_exist_error(e: requests.HTTPError) -> bool:
|
|
2489
|
+
"""
|
|
2490
|
+
Check if the error is a known 404 Not Found error.
|
|
2491
|
+
|
|
2492
|
+
Args:
|
|
2493
|
+
e: HTTPError to check.
|
|
2494
|
+
|
|
2495
|
+
Returns:
|
|
2496
|
+
True if the error is a 404 Not Found error, False otherwise.
|
|
2497
|
+
"""
|
|
2498
|
+
if (
|
|
2499
|
+
# Check whether the error is caused by a 404 status code - meaning the app does not exist.
|
|
2500
|
+
(hasattr(e, "response") and hasattr(e.response, "status_code") and e.response.status_code == 404)
|
|
2501
|
+
or
|
|
2502
|
+
# Check a possibly nested exception as well.
|
|
2503
|
+
(
|
|
2504
|
+
hasattr(e, "__cause__")
|
|
2505
|
+
and hasattr(e.__cause__, "response")
|
|
2506
|
+
and hasattr(e.__cause__.response, "status_code")
|
|
2507
|
+
and e.__cause__.response.status_code == 404
|
|
2508
|
+
)
|
|
2509
|
+
):
|
|
2510
|
+
return True
|
|
2511
|
+
return False
|
nextmv/cloud/run.py
CHANGED
|
@@ -14,6 +14,38 @@ from nextmv.input import Input, InputFormat
|
|
|
14
14
|
from nextmv.output import Output, OutputFormat
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def run_duration(
|
|
18
|
+
start: Union[datetime, float],
|
|
19
|
+
end: Union[datetime, float],
|
|
20
|
+
) -> int:
|
|
21
|
+
"""
|
|
22
|
+
Calculate the duration of a run in milliseconds.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
start : Union[datetime, float]
|
|
27
|
+
The start time of the run. Can be a datetime object or a float
|
|
28
|
+
representing the start time in seconds since the epoch.
|
|
29
|
+
end : Union[datetime, float]
|
|
30
|
+
The end time of the run. Can be a datetime object or a float
|
|
31
|
+
representing the end time in seconds since the epoch.
|
|
32
|
+
|
|
33
|
+
Returns
|
|
34
|
+
-------
|
|
35
|
+
int
|
|
36
|
+
The duration of the run in milliseconds.
|
|
37
|
+
"""
|
|
38
|
+
if isinstance(start, float) and isinstance(end, float):
|
|
39
|
+
if start > end:
|
|
40
|
+
raise ValueError("Start time must be before end time.")
|
|
41
|
+
return int(round((end - start) * 1000))
|
|
42
|
+
if isinstance(start, datetime) and isinstance(end, datetime):
|
|
43
|
+
if start > end:
|
|
44
|
+
raise ValueError("Start time must be before end time.")
|
|
45
|
+
return int(round((end - start).total_seconds() * 1000))
|
|
46
|
+
raise TypeError("Start and end must be either datetime or float.")
|
|
47
|
+
|
|
48
|
+
|
|
17
49
|
class Metadata(BaseModel):
|
|
18
50
|
"""Metadata of a run, whether it was successful or not."""
|
|
19
51
|
|
|
@@ -181,7 +213,7 @@ class ExternalRunResult(BaseModel):
|
|
|
181
213
|
error_message: Optional[str] = None
|
|
182
214
|
"""Error message of the run."""
|
|
183
215
|
execution_duration: Optional[int] = None
|
|
184
|
-
"""Duration of the run, in
|
|
216
|
+
"""Duration of the run, in milliseconds."""
|
|
185
217
|
|
|
186
218
|
def __post_init_post_parse__(self):
|
|
187
219
|
"""Validations done after parsing the model."""
|
|
@@ -246,7 +278,7 @@ class TrackedRun:
|
|
|
246
278
|
"""The status of the run being tracked"""
|
|
247
279
|
|
|
248
280
|
duration: Optional[int] = None
|
|
249
|
-
"""The duration of the run being tracked, in
|
|
281
|
+
"""The duration of the run being tracked, in milliseconds."""
|
|
250
282
|
error: Optional[str] = None
|
|
251
283
|
"""An error message if the run failed. You should only specify this if the
|
|
252
284
|
run failed, otherwise an exception will be raised."""
|
nextmv/logger.py
CHANGED
|
@@ -3,13 +3,17 @@
|
|
|
3
3
|
import sys
|
|
4
4
|
|
|
5
5
|
__original_stdout = None
|
|
6
|
+
__stdout_redirected = False
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
def redirect_stdout() -> None:
|
|
9
10
|
"""Redirect all messages written to stdout to stderr. When you do not want
|
|
10
11
|
to redirect stdout anymore, call `reset_stdout`."""
|
|
11
12
|
|
|
12
|
-
global __original_stdout
|
|
13
|
+
global __original_stdout, __stdout_redirected
|
|
14
|
+
if __stdout_redirected:
|
|
15
|
+
return
|
|
16
|
+
__stdout_redirected = True
|
|
13
17
|
|
|
14
18
|
__original_stdout = sys.stdout
|
|
15
19
|
sys.stdout = sys.stderr
|
|
@@ -19,7 +23,10 @@ def reset_stdout() -> None:
|
|
|
19
23
|
"""Reset stdout to its original value. This function should always be
|
|
20
24
|
called after `redirect_stdout` to avoid unexpected behavior."""
|
|
21
25
|
|
|
22
|
-
global __original_stdout
|
|
26
|
+
global __original_stdout, __stdout_redirected
|
|
27
|
+
if not __stdout_redirected:
|
|
28
|
+
return
|
|
29
|
+
__stdout_redirected = False
|
|
23
30
|
|
|
24
31
|
if __original_stdout is None:
|
|
25
32
|
sys.stdout = sys.__stdout__
|
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
nextmv/__about__.py,sha256=
|
|
1
|
+
nextmv/__about__.py,sha256=MxqQiLafrvDHqguGRsR2SDY95UETqLzJGiFRY6QwEmk,24
|
|
2
2
|
nextmv/__entrypoint__.py,sha256=5K058PICm5sx4sNMqx56auMh9yWgdIESVLzfvyIXdjs,1158
|
|
3
3
|
nextmv/__init__.py,sha256=QN5e_BFkIdBkR8DiGr9T06N6mXtowT84eRUJj3_Vfrg,1459
|
|
4
4
|
nextmv/base_model.py,sha256=mdaBe-epNK1cFgP4TxbOtn3So4pCi1vMTOrIBkCBp7A,1050
|
|
5
5
|
nextmv/deprecated.py,sha256=ctE39pmJEfoicR-w0EdT7FPtd0cWmP3GKth-FsNn_Ks,406
|
|
6
6
|
nextmv/input.py,sha256=tppXHiJM_EzR9Z1yJPc37joBvgC0RmiAFo0Ab1X5nPA,15480
|
|
7
|
-
nextmv/logger.py,sha256=
|
|
7
|
+
nextmv/logger.py,sha256=Jo_AmYJ02WqXmsz9XTTOXZ9uenVHrPanMMqhBogxGCw,1075
|
|
8
8
|
nextmv/model.py,sha256=rwBdgmKSEp1DPv43zF0azj3QnbHO6O6wKs0PIGvVS40,9861
|
|
9
9
|
nextmv/options.py,sha256=IPqAIUjoKMWmoPx_e5zDcnxu7S2HVxw-SLjjUduVvZM,25302
|
|
10
10
|
nextmv/output.py,sha256=rcmDivGRsInyS944ivO6SBtVFSH3RC5Nwu0qFTFRRLI,22270
|
|
11
|
-
nextmv/cloud/__init__.py,sha256=
|
|
11
|
+
nextmv/cloud/__init__.py,sha256=EEKJdSMMjW2yc9GwTpz-cmpgtu-Qs7p09k6NXIQPV0U,3726
|
|
12
12
|
nextmv/cloud/acceptance_test.py,sha256=NtqGhj-UYibxGBbU2kfjr-lYcngojb_5VMvK2WZwibI,6620
|
|
13
13
|
nextmv/cloud/account.py,sha256=mZUGzV-uMGBA5BC_FPtsiCMFuz5jxEZ3O1BbELZIm18,1841
|
|
14
|
-
nextmv/cloud/application.py,sha256=
|
|
14
|
+
nextmv/cloud/application.py,sha256=0mTMSIze3uXMlWY7K7OYicMVox0IaC0PapLx41iVP2c,84098
|
|
15
15
|
nextmv/cloud/batch_experiment.py,sha256=Ngm1XDvvAMaU9VYU5JFNxXFnt8xjE_34H5W54kO9V5c,3768
|
|
16
16
|
nextmv/cloud/client.py,sha256=JUE3vD767_FFICl1vov5Mxmircci03TBL3KmT1BOZY4,9096
|
|
17
17
|
nextmv/cloud/input_set.py,sha256=HTLA2acJridZbBCAVJNom8ldNo2Rfy40YQ8Coyz3bdo,1482
|
|
18
18
|
nextmv/cloud/instance.py,sha256=UfyfZXfL1ugCGAB6zwZJIAi8qxI1JCUGsluwaGdjfN4,1223
|
|
19
19
|
nextmv/cloud/manifest.py,sha256=lB3rwFmIKNAncaA00DelT4tDkMkIUCWEY4QJKJsx_-U,12116
|
|
20
20
|
nextmv/cloud/package.py,sha256=Y3RethLiXXW7Y6_QBJDJdd7mFNaYaSBMyasIeGLav8o,12287
|
|
21
|
-
nextmv/cloud/run.py,sha256=
|
|
21
|
+
nextmv/cloud/run.py,sha256=K3zIoV7s39WO6rd882h3EM3a6fDPiiHk99zR9K8QobM,10721
|
|
22
22
|
nextmv/cloud/safe.py,sha256=IUQUOlx4ulIIyP2-Fx3-gNBm9nyWOgZgaOAlRrzqaZE,2515
|
|
23
23
|
nextmv/cloud/scenario.py,sha256=9gbdnQuvmerPUBCcJ-5QbLCwgbsIfBwKXE8c5359S8E,8120
|
|
24
24
|
nextmv/cloud/secrets.py,sha256=kqlN4ceww_L4kVTrAU8BZykRzXINO3zhMT_BLYea6tk,1764
|
|
25
25
|
nextmv/cloud/status.py,sha256=C-ax8cLw0jPeh7CPsJkCa0s4ImRyFI4NDJJxI0_1sr4,602
|
|
26
26
|
nextmv/cloud/version.py,sha256=sjVRNRtohHA97j6IuyM33_DSSsXYkZPusYgpb6hlcrc,1244
|
|
27
|
-
nextmv-0.26.
|
|
28
|
-
nextmv-0.26.
|
|
29
|
-
nextmv-0.26.
|
|
30
|
-
nextmv-0.26.
|
|
27
|
+
nextmv-0.26.1.dist-info/METADATA,sha256=fHq_8Cren23n3msctySfLJMtnOl4kpC2iEmSE6Ug2t0,14557
|
|
28
|
+
nextmv-0.26.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
29
|
+
nextmv-0.26.1.dist-info/licenses/LICENSE,sha256=ZIbK-sSWA-OZprjNbmJAglYRtl5_K4l9UwAV3PGJAPc,11349
|
|
30
|
+
nextmv-0.26.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|