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 CHANGED
@@ -1 +1 @@
1
- __version__ = "v0.26.0"
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
@@ -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 seconds."""
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 seconds."""
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nextmv
3
- Version: 0.26.0
3
+ Version: 0.26.1
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://www.nextmv.io/docs/python-sdks/nextmv/installation
@@ -1,30 +1,30 @@
1
- nextmv/__about__.py,sha256=eQqPiK0rRHZ8N7lI25IAshYTaILxk2NZzxVXgTPB1OY,24
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=5qQ7E3Aaw3zzkIeiUuwYGzTcB7VhVqIzNZm5PHmdpUI,852
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=O34rjrgClnIwXDE63PK4iq5ESuGQ86dJ6FPSrcUnmk0,3680
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=BoW-n_cJ4nxpmxOq_VMdJzv7B4RSxRhApkv0_oEcEYQ,81952
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=yeuaY8rAxTAhhKY0zTGQA3sKUkSiJg487QZIxPppm40,9604
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.0.dist-info/METADATA,sha256=c53wSmLfDD0omHMTPlxDkrx9DMb5RdvKMNHCByar1TA,14557
28
- nextmv-0.26.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
- nextmv-0.26.0.dist-info/licenses/LICENSE,sha256=ZIbK-sSWA-OZprjNbmJAglYRtl5_K4l9UwAV3PGJAPc,11349
30
- nextmv-0.26.0.dist-info/RECORD,,
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,,