nextmv 0.20.1__py3-none-any.whl → 0.21.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.20.1"
1
+ __version__ = "v0.21.1"
nextmv/cloud/__init__.py CHANGED
@@ -48,6 +48,8 @@ from .run import RunLog as RunLog
48
48
  from .run import RunResult as RunResult
49
49
  from .run import RunType as RunType
50
50
  from .run import RunTypeConfiguration as RunTypeConfiguration
51
+ from .run import TrackedRun as TrackedRun
52
+ from .run import TrackedRunStatus as TrackedRunStatus
51
53
  from .secrets import Secret as Secret
52
54
  from .secrets import SecretsCollection as SecretsCollection
53
55
  from .secrets import SecretsCollectionSummary as SecretsCollectionSummary
@@ -18,12 +18,14 @@ from nextmv.cloud.client import Client, get_size
18
18
  from nextmv.cloud.input_set import InputSet
19
19
  from nextmv.cloud.instance import Instance, InstanceConfiguration
20
20
  from nextmv.cloud.manifest import Manifest
21
- from nextmv.cloud.run import ExternalRunResult, RunConfiguration, RunInformation, RunLog, RunResult
21
+ from nextmv.cloud.run import ExternalRunResult, RunConfiguration, RunInformation, RunLog, RunResult, TrackedRun
22
22
  from nextmv.cloud.secrets import Secret, SecretsCollection, SecretsCollectionSummary
23
23
  from nextmv.cloud.status import StatusV2
24
24
  from nextmv.cloud.version import Version
25
+ from nextmv.input import Input
25
26
  from nextmv.logger import log
26
27
  from nextmv.model import Model, ModelConfiguration
28
+ from nextmv.output import Output
27
29
 
28
30
  _MAX_RUN_SIZE: int = 5 * 1024 * 1024
29
31
  """Maximum size of the run input/output. This value is used to determine
@@ -441,6 +443,7 @@ class Application:
441
443
  id: Optional[str] = None,
442
444
  description: Optional[str] = None,
443
445
  is_workflow: Optional[bool] = None,
446
+ exist_ok: bool = False,
444
447
  ) -> "Application":
445
448
  """
446
449
  Create a new application.
@@ -456,6 +459,9 @@ class Application:
456
459
  The new application.
457
460
  """
458
461
 
462
+ if exist_ok and cls.exists(client=client, id=id):
463
+ return Application(client=client, id=id)
464
+
459
465
  payload = {
460
466
  "name": name,
461
467
  }
@@ -735,8 +741,8 @@ class Application:
735
741
  def new_instance(
736
742
  self,
737
743
  version_id: str,
738
- id: Optional[str] = None,
739
- name: Optional[str] = None,
744
+ id: str,
745
+ name: str,
740
746
  description: Optional[str] = None,
741
747
  configuration: Optional[InstanceConfiguration] = None,
742
748
  ) -> Instance:
@@ -934,8 +940,8 @@ class Application:
934
940
  def new_secrets_collection(
935
941
  self,
936
942
  secrets: list[Secret],
937
- id: Optional[str] = None,
938
- name: Optional[str] = None,
943
+ id: str,
944
+ name: str,
939
945
  description: Optional[str] = None,
940
946
  ) -> SecretsCollectionSummary:
941
947
  """
@@ -1284,6 +1290,107 @@ class Application:
1284
1290
 
1285
1291
  return self.__run_result(run_id=run_id, run_information=run_information)
1286
1292
 
1293
+ def track_run(self, tracked_run: TrackedRun) -> str:
1294
+ """
1295
+ Track an external run.
1296
+
1297
+ This method allows you to register in Nextmv a run that happened
1298
+ elsewhere, as though it were executed in the Nextmv platform. Having
1299
+ information about a run in Nextmv is useful for things like
1300
+ experimenting and testing.
1301
+
1302
+ Parameters
1303
+ ----------
1304
+ tracked_run : TrackedRun
1305
+ The run to track.
1306
+
1307
+ Returns
1308
+ -------
1309
+ str
1310
+ The ID of the run that was tracked.
1311
+
1312
+ Raises
1313
+ ------
1314
+ requests.HTTPError
1315
+ If the response status code is not 2xx.
1316
+ ValueError
1317
+ If the tracked run does not have an input or output.
1318
+ """
1319
+ url_input = self.upload_url()
1320
+
1321
+ upload_input = tracked_run.input
1322
+ if isinstance(tracked_run.input, Input):
1323
+ upload_input = tracked_run.input.to_dict()
1324
+
1325
+ self.upload_large_input(input=upload_input, upload_url=url_input)
1326
+
1327
+ url_output = self.upload_url()
1328
+
1329
+ upload_output = tracked_run.output
1330
+ if isinstance(tracked_run.output, Output):
1331
+ upload_output = tracked_run.output.to_dict()
1332
+
1333
+ self.upload_large_input(input=upload_output, upload_url=url_output)
1334
+
1335
+ external_result = ExternalRunResult(
1336
+ output_upload_id=url_output.upload_id,
1337
+ status=tracked_run.status.value,
1338
+ execution_duration=tracked_run.duration,
1339
+ )
1340
+
1341
+ if tracked_run.logs is not None:
1342
+ url_stderr = self.upload_url()
1343
+ self.upload_large_input(input=tracked_run.logs_text(), upload_url=url_stderr)
1344
+ external_result.error_upload_id = url_stderr.upload_id
1345
+
1346
+ if tracked_run.error is not None and tracked_run.error != "":
1347
+ external_result.error_message = tracked_run.error
1348
+
1349
+ return self.new_run(upload_id=url_input.upload_id, external_result=external_result)
1350
+
1351
+ def track_run_with_result(
1352
+ self,
1353
+ tracked_run: TrackedRun,
1354
+ polling_options: PollingOptions = _DEFAULT_POLLING_OPTIONS,
1355
+ ) -> RunResult:
1356
+ """
1357
+ Track an external run and poll for the result. This is a convenience
1358
+ method that combines the `track_run` and `run_result_with_polling`
1359
+ methods. It applies polling logic to check when the run was
1360
+ successfully registered.
1361
+
1362
+ Parameters
1363
+ ----------
1364
+ tracked_run : TrackedRun
1365
+ The run to track.
1366
+ polling_options : PollingOptions
1367
+ Options to use when polling for the run result.
1368
+
1369
+ Returns
1370
+ -------
1371
+ RunResult
1372
+ Result of the run.
1373
+
1374
+ Raises
1375
+ ------
1376
+ requests.HTTPError
1377
+ If the response status code is not 2xx.
1378
+ ValueError
1379
+ If the tracked run does not have an input or output.
1380
+ TimeoutError
1381
+ If the run does not succeed after the polling strategy is
1382
+ exhausted based on time duration.
1383
+ RuntimeError
1384
+ If the run does not succeed after the polling strategy is
1385
+ exhausted based on number of tries.
1386
+ """
1387
+ run_id = self.track_run(tracked_run=tracked_run)
1388
+
1389
+ return self.run_result_with_polling(
1390
+ run_id=run_id,
1391
+ polling_options=polling_options,
1392
+ )
1393
+
1287
1394
  def update_instance(
1288
1395
  self,
1289
1396
  id: str,
@@ -1334,7 +1441,7 @@ class Application:
1334
1441
  name: str,
1335
1442
  description: str,
1336
1443
  secrets: list[Secret],
1337
- ) -> SecretsCollection:
1444
+ ) -> SecretsCollectionSummary:
1338
1445
  """
1339
1446
  Update a secrets collection.
1340
1447
 
nextmv/cloud/run.py CHANGED
@@ -1,14 +1,17 @@
1
1
  """This module contains definitions for an app run."""
2
2
 
3
+ import json
4
+ from dataclasses import dataclass
3
5
  from datetime import datetime
4
6
  from enum import Enum
5
- from typing import Any, Optional
7
+ from typing import Any, Optional, Union
6
8
 
7
9
  from pydantic import AliasChoices, Field
8
10
 
9
11
  from nextmv.base_model import BaseModel
10
12
  from nextmv.cloud.status import Status, StatusV2
11
- from nextmv.input import InputFormat
13
+ from nextmv.input import Input, InputFormat
14
+ from nextmv.output import Output, OutputFormat
12
15
 
13
16
 
14
17
  class Metadata(BaseModel):
@@ -159,3 +162,114 @@ class ExternalRunResult(BaseModel):
159
162
  valid_statuses = {"succeeded", "failed"}
160
163
  if self.status is not None and self.status not in valid_statuses:
161
164
  raise ValueError("Invalid status value, must be one of: " + ", ".join(valid_statuses))
165
+
166
+
167
+ class TrackedRunStatus(str, Enum):
168
+ """
169
+ The status of a tracked run.
170
+
171
+ Attributes
172
+ ----------
173
+ SUCCEEDED : str
174
+ The run succeeded.
175
+ FAILED : str
176
+ The run failed.
177
+ """
178
+
179
+ SUCCEEDED = "succeeded"
180
+ """The run succeeded."""
181
+ FAILED = "failed"
182
+ """The run failed."""
183
+
184
+
185
+ @dataclass
186
+ class TrackedRun:
187
+ """
188
+ An external run that is tracked in the Nextmv platform.
189
+
190
+ Attributes
191
+ ----------
192
+ input : Union[Input, dict[str, any], str]
193
+ The input of the run being tracked. Please note that if the input
194
+ format is JSON, then the input data must be JSON serializable. This
195
+ field is required.
196
+ output : Union[Output, dict[str, any], str]
197
+ The output of the run being tracked. Please note that if the output
198
+ format is JSON, then the output data must be JSON serializable. This
199
+ field is required.
200
+ status : TrackedRunStatus
201
+ The status of the run being tracked. This field is required.
202
+ duration : Optional[int]
203
+ The duration of the run being tracked, in seconds. This field is
204
+ optional.
205
+ error : Optional[str]
206
+ An error message if the run failed. You should only specify this if the
207
+ run failed (the `status` is `TrackedRunStatus.FAILED`), otherwise an
208
+ exception will be raised. This field is optional.
209
+ logs : Optional[list[str]]
210
+ The logs of the run being tracked. Each element of the list is a line in
211
+ the log. This field is optional.
212
+ """
213
+
214
+ input: Union[Input, dict[str, any], str]
215
+ """The input of the run being tracked."""
216
+ output: Union[Output, dict[str, any], str]
217
+ """The output of the run being tracked. Only JSON output_format is supported."""
218
+ status: TrackedRunStatus
219
+ """The status of the run being tracked"""
220
+
221
+ duration: Optional[int] = None
222
+ """The duration of the run being tracked, in seconds."""
223
+ error: Optional[str] = None
224
+ """An error message if the run failed. You should only specify this if the
225
+ run failed, otherwise an exception will be raised."""
226
+ logs: Optional[list[str]] = None
227
+ """The logs of the run being tracked. Each element of the list is a line in
228
+ the log."""
229
+
230
+ def __post_init__(self): # noqa: C901
231
+ """Validations done after parsing the model."""
232
+
233
+ valid_statuses = {TrackedRunStatus.SUCCEEDED, TrackedRunStatus.FAILED}
234
+ if self.status not in valid_statuses:
235
+ raise ValueError("Invalid status value, must be one of: " + ", ".join(valid_statuses))
236
+
237
+ if self.error is not None and self.error != "" and self.status != TrackedRunStatus.FAILED:
238
+ raise ValueError("Error message must be empty if the run succeeded.")
239
+
240
+ if isinstance(self.input, Input):
241
+ if self.input.input_format != InputFormat.JSON:
242
+ raise ValueError("Input.input_format must be JSON.")
243
+ elif isinstance(self.input, dict):
244
+ try:
245
+ _ = json.dumps(self.input)
246
+ except (TypeError, OverflowError) as e:
247
+ raise ValueError("Input is dict[str, any] but it is not JSON serializable") from e
248
+
249
+ if isinstance(self.output, Output):
250
+ if self.output.output_format != OutputFormat.JSON:
251
+ raise ValueError("Output.output_format must be JSON.")
252
+ elif isinstance(self.output, dict):
253
+ try:
254
+ _ = json.dumps(self.output)
255
+ except (TypeError, OverflowError) as e:
256
+ raise ValueError("Output is dict[str, any] but it is not JSON serializable") from e
257
+
258
+ def logs_text(self) -> str:
259
+ """
260
+ Returns the logs as a single string.
261
+
262
+ Parameters
263
+ ----------
264
+ None
265
+
266
+ Returns
267
+ -------
268
+ str
269
+ The logs as a single string.
270
+ """
271
+
272
+ if self.logs is None:
273
+ return ""
274
+
275
+ return "\n".join(self.logs)
nextmv/input.py CHANGED
@@ -91,6 +91,26 @@ class Input:
91
91
  new_options = copy.deepcopy(init_options)
92
92
  self.options = new_options
93
93
 
94
+ def to_dict(self) -> dict[str, any]:
95
+ """
96
+ Convert the input to a dictionary.
97
+
98
+ Parameters
99
+ ----------
100
+ None
101
+
102
+ Returns
103
+ -------
104
+ dict[str, any]
105
+ The input as a dictionary.
106
+ """
107
+
108
+ return {
109
+ "data": self.data,
110
+ "input_format": self.input_format.value,
111
+ "options": self.options.to_dict() if self.options is not None else None,
112
+ }
113
+
94
114
 
95
115
  class InputLoader:
96
116
  """Base class for loading inputs."""
nextmv/output.py CHANGED
@@ -322,6 +322,29 @@ class Output:
322
322
  "output_format OutputFormat.CSV_ARCHIVE, supported type is `dict`"
323
323
  )
324
324
 
325
+ def to_dict(self) -> dict[str, any]:
326
+ """
327
+ Convert the `Output` object to a dictionary.
328
+
329
+ Returns
330
+ -------
331
+ dict[str, any]
332
+ The dictionary representation of the `Output` object.
333
+ """
334
+
335
+ output_dict = {
336
+ "options": self.options.to_dict() if self.options is not None else None,
337
+ "output_format": self.output_format,
338
+ "solution": self.solution,
339
+ "statistics": self.statistics.to_dict() if self.statistics is not None else None,
340
+ "assets": [asset.to_dict() for asset in self.assets] if self.assets is not None else None,
341
+ }
342
+
343
+ if self.output_format == OutputFormat.CSV_ARCHIVE:
344
+ output_dict["csv_configurations"] = self.csv_configurations
345
+
346
+ return output_dict
347
+
325
348
 
326
349
  class OutputWriter:
327
350
  """Base class for writing outputs."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nextmv
3
- Version: 0.20.1
3
+ Version: 0.21.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,27 +1,27 @@
1
- nextmv/__about__.py,sha256=KqK0kkAlPQdD5W8qM68QyM56Jy5p0vIV7skZlBkTMMo,24
1
+ nextmv/__about__.py,sha256=1EcNvwMaSDEkTRoL7sc_hMTild6kSCT-JOUHMHTrrw8,24
2
2
  nextmv/__entrypoint__.py,sha256=o6xYGBBUgCY_htteW-qNJfp-S3Th8dzS4RreJcDCnzM,1333
3
3
  nextmv/__init__.py,sha256=rz9oP7JGyFQJdUtYX4j9xx4Gv4LMGfNvXoEID5qUqig,1354
4
4
  nextmv/base_model.py,sha256=mdaBe-epNK1cFgP4TxbOtn3So4pCi1vMTOrIBkCBp7A,1050
5
- nextmv/input.py,sha256=ukP9R8o1zh6viP_Hv0jBivdPQ3K7Fy-PgUbxikUBpKg,12279
5
+ nextmv/input.py,sha256=tYjiD9KM37Phqzm4faO4QLem8IXnU6HS160c1E-Yhc8,12732
6
6
  nextmv/logger.py,sha256=5qQ7E3Aaw3zzkIeiUuwYGzTcB7VhVqIzNZm5PHmdpUI,852
7
7
  nextmv/model.py,sha256=rwBdgmKSEp1DPv43zF0azj3QnbHO6O6wKs0PIGvVS40,9861
8
8
  nextmv/options.py,sha256=DZmjNxZyaTwnG2MEEKbIiBx3hI5F2DG70rY7DdeX44M,17336
9
- nextmv/output.py,sha256=YgRs9I7SEhpKzJPALfy09ew9wdVRX-uhwBsVsnB0bL4,19276
10
- nextmv/cloud/__init__.py,sha256=inttQhr3SEibNebMDNJzfVrvOLc4_fAR6zmwabjb7QQ,3209
9
+ nextmv/output.py,sha256=cxw7Z0dTj5u42w2MQLOz2gesj046tvYFSWmhSZtRSXU,20082
10
+ nextmv/cloud/__init__.py,sha256=ojgA2vQRQnR8QO2tJme1TbGrjhr9yq9LIauxKJ5F40U,3305
11
11
  nextmv/cloud/acceptance_test.py,sha256=NtqGhj-UYibxGBbU2kfjr-lYcngojb_5VMvK2WZwibI,6620
12
12
  nextmv/cloud/account.py,sha256=mZUGzV-uMGBA5BC_FPtsiCMFuz5jxEZ3O1BbELZIm18,1841
13
- nextmv/cloud/application.py,sha256=a_fuG_qyjCwjjx9elMHXM_2qkRniOOyMSMGcsLkzdeM,52409
13
+ nextmv/cloud/application.py,sha256=VQfi7Acv19ezAUM6ODj4NsIka8XYB3ibgOkuKvkuQNw,55909
14
14
  nextmv/cloud/batch_experiment.py,sha256=UxrMNAm2c7-RZ9TWRhZBtiVyEeUG1pjsLNhYkoT5Jog,2236
15
15
  nextmv/cloud/client.py,sha256=E4C8wOGOdgyMf-DCDt4YfI7ib8HwmUhAtKMdrSBJc2M,9004
16
16
  nextmv/cloud/input_set.py,sha256=ovkP17-jYs0yWrbqTM6Nl5ubWQabD_UrDqAHNo8aE2s,672
17
17
  nextmv/cloud/instance.py,sha256=UfyfZXfL1ugCGAB6zwZJIAi8qxI1JCUGsluwaGdjfN4,1223
18
18
  nextmv/cloud/manifest.py,sha256=Fdjw4c14_ph20ASOzBs2mLslyLs1jGSxWThipGFp8rk,7458
19
19
  nextmv/cloud/package.py,sha256=Y3RethLiXXW7Y6_QBJDJdd7mFNaYaSBMyasIeGLav8o,12287
20
- nextmv/cloud/run.py,sha256=93o5SJMvN2b-Qd2ZGoXuelAHafKsGRt-DyOtE4srTsQ,4672
20
+ nextmv/cloud/run.py,sha256=4hEANGXysDiOI1SQtfs7t_IVk-bxCldIs2TOvCI7a3E,8689
21
21
  nextmv/cloud/secrets.py,sha256=kqlN4ceww_L4kVTrAU8BZykRzXINO3zhMT_BLYea6tk,1764
22
22
  nextmv/cloud/status.py,sha256=C-ax8cLw0jPeh7CPsJkCa0s4ImRyFI4NDJJxI0_1sr4,602
23
23
  nextmv/cloud/version.py,sha256=sjVRNRtohHA97j6IuyM33_DSSsXYkZPusYgpb6hlcrc,1244
24
- nextmv-0.20.1.dist-info/METADATA,sha256=pv0uEyHcZehvo3sdsMhPfo1N1Vn0znpp7EbQUEpYJ98,14557
25
- nextmv-0.20.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
- nextmv-0.20.1.dist-info/licenses/LICENSE,sha256=ZIbK-sSWA-OZprjNbmJAglYRtl5_K4l9UwAV3PGJAPc,11349
27
- nextmv-0.20.1.dist-info/RECORD,,
24
+ nextmv-0.21.1.dist-info/METADATA,sha256=fLNFHuNeJvQST1BsX0vY7UmqrkFtrb5h8sp0ptN6ew4,14557
25
+ nextmv-0.21.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
+ nextmv-0.21.1.dist-info/licenses/LICENSE,sha256=ZIbK-sSWA-OZprjNbmJAglYRtl5_K4l9UwAV3PGJAPc,11349
27
+ nextmv-0.21.1.dist-info/RECORD,,