nextmv 0.20.1__py3-none-any.whl → 0.21.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 CHANGED
@@ -1 +1 @@
1
- __version__ = "v0.20.1"
1
+ __version__ = "v0.21.0"
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
@@ -735,8 +737,8 @@ class Application:
735
737
  def new_instance(
736
738
  self,
737
739
  version_id: str,
738
- id: Optional[str] = None,
739
- name: Optional[str] = None,
740
+ id: str,
741
+ name: str,
740
742
  description: Optional[str] = None,
741
743
  configuration: Optional[InstanceConfiguration] = None,
742
744
  ) -> Instance:
@@ -934,8 +936,8 @@ class Application:
934
936
  def new_secrets_collection(
935
937
  self,
936
938
  secrets: list[Secret],
937
- id: Optional[str] = None,
938
- name: Optional[str] = None,
939
+ id: str,
940
+ name: str,
939
941
  description: Optional[str] = None,
940
942
  ) -> SecretsCollectionSummary:
941
943
  """
@@ -1284,6 +1286,107 @@ class Application:
1284
1286
 
1285
1287
  return self.__run_result(run_id=run_id, run_information=run_information)
1286
1288
 
1289
+ def track_run(self, tracked_run: TrackedRun) -> str:
1290
+ """
1291
+ Track an external run.
1292
+
1293
+ This method allows you to register in Nextmv a run that happened
1294
+ elsewhere, as though it were executed in the Nextmv platform. Having
1295
+ information about a run in Nextmv is useful for things like
1296
+ experimenting and testing.
1297
+
1298
+ Parameters
1299
+ ----------
1300
+ tracked_run : TrackedRun
1301
+ The run to track.
1302
+
1303
+ Returns
1304
+ -------
1305
+ str
1306
+ The ID of the run that was tracked.
1307
+
1308
+ Raises
1309
+ ------
1310
+ requests.HTTPError
1311
+ If the response status code is not 2xx.
1312
+ ValueError
1313
+ If the tracked run does not have an input or output.
1314
+ """
1315
+ url_input = self.upload_url()
1316
+
1317
+ upload_input = tracked_run.input
1318
+ if isinstance(tracked_run.input, Input):
1319
+ upload_input = tracked_run.input.to_dict()
1320
+
1321
+ self.upload_large_input(input=upload_input, upload_url=url_input)
1322
+
1323
+ url_output = self.upload_url()
1324
+
1325
+ upload_output = tracked_run.output
1326
+ if isinstance(tracked_run.output, Output):
1327
+ upload_output = tracked_run.output.to_dict()
1328
+
1329
+ self.upload_large_input(input=upload_output, upload_url=url_output)
1330
+
1331
+ external_result = ExternalRunResult(
1332
+ output_upload_id=url_output.upload_id,
1333
+ status=tracked_run.status.value,
1334
+ execution_duration=tracked_run.duration,
1335
+ )
1336
+
1337
+ if tracked_run.logs is not None:
1338
+ url_stderr = self.upload_url()
1339
+ self.upload_large_input(input=tracked_run.logs_text(), upload_url=url_stderr)
1340
+ external_result.error_upload_id = url_stderr.upload_id
1341
+
1342
+ if tracked_run.error is not None and tracked_run.error != "":
1343
+ external_result.error_message = tracked_run.error
1344
+
1345
+ return self.new_run(upload_id=url_input.upload_id, external_result=external_result)
1346
+
1347
+ def track_run_with_result(
1348
+ self,
1349
+ tracked_run: TrackedRun,
1350
+ polling_options: PollingOptions = _DEFAULT_POLLING_OPTIONS,
1351
+ ) -> RunResult:
1352
+ """
1353
+ Track an external run and poll for the result. This is a convenience
1354
+ method that combines the `track_run` and `run_result_with_polling`
1355
+ methods. It applies polling logic to check when the run was
1356
+ successfully registered.
1357
+
1358
+ Parameters
1359
+ ----------
1360
+ tracked_run : TrackedRun
1361
+ The run to track.
1362
+ polling_options : PollingOptions
1363
+ Options to use when polling for the run result.
1364
+
1365
+ Returns
1366
+ -------
1367
+ RunResult
1368
+ Result of the run.
1369
+
1370
+ Raises
1371
+ ------
1372
+ requests.HTTPError
1373
+ If the response status code is not 2xx.
1374
+ ValueError
1375
+ If the tracked run does not have an input or output.
1376
+ TimeoutError
1377
+ If the run does not succeed after the polling strategy is
1378
+ exhausted based on time duration.
1379
+ RuntimeError
1380
+ If the run does not succeed after the polling strategy is
1381
+ exhausted based on number of tries.
1382
+ """
1383
+ run_id = self.track_run(tracked_run=tracked_run)
1384
+
1385
+ return self.run_result_with_polling(
1386
+ run_id=run_id,
1387
+ polling_options=polling_options,
1388
+ )
1389
+
1287
1390
  def update_instance(
1288
1391
  self,
1289
1392
  id: str,
@@ -1334,7 +1437,7 @@ class Application:
1334
1437
  name: str,
1335
1438
  description: str,
1336
1439
  secrets: list[Secret],
1337
- ) -> SecretsCollection:
1440
+ ) -> SecretsCollectionSummary:
1338
1441
  """
1339
1442
  Update a secrets collection.
1340
1443
 
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.0
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=cVvubA2yQRXFVwQqlSnbiwiGxtKCAPt8AmgS0WPjXVw,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=cmgXz9J9pdugPVqTioqzXh_1iidY0Gcbr11e_JLat3E,55765
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.0.dist-info/METADATA,sha256=GYySweVUa_akR4revy2w1dgfgBoPJBD7q4AL95EiIeE,14557
25
+ nextmv-0.21.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
26
+ nextmv-0.21.0.dist-info/licenses/LICENSE,sha256=ZIbK-sSWA-OZprjNbmJAglYRtl5_K4l9UwAV3PGJAPc,11349
27
+ nextmv-0.21.0.dist-info/RECORD,,