nextmv 0.18.0__py3-none-any.whl → 1.0.0.dev2__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.
Files changed (175) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__entrypoint__.py +8 -13
  3. nextmv/__init__.py +53 -0
  4. nextmv/_serialization.py +96 -0
  5. nextmv/base_model.py +54 -9
  6. nextmv/cli/CONTRIBUTING.md +511 -0
  7. nextmv/cli/__init__.py +0 -0
  8. nextmv/cli/cloud/__init__.py +47 -0
  9. nextmv/cli/cloud/acceptance/__init__.py +27 -0
  10. nextmv/cli/cloud/acceptance/create.py +393 -0
  11. nextmv/cli/cloud/acceptance/delete.py +68 -0
  12. nextmv/cli/cloud/acceptance/get.py +104 -0
  13. nextmv/cli/cloud/acceptance/list.py +62 -0
  14. nextmv/cli/cloud/acceptance/update.py +95 -0
  15. nextmv/cli/cloud/account/__init__.py +28 -0
  16. nextmv/cli/cloud/account/create.py +83 -0
  17. nextmv/cli/cloud/account/delete.py +60 -0
  18. nextmv/cli/cloud/account/get.py +66 -0
  19. nextmv/cli/cloud/account/update.py +70 -0
  20. nextmv/cli/cloud/app/__init__.py +35 -0
  21. nextmv/cli/cloud/app/create.py +141 -0
  22. nextmv/cli/cloud/app/delete.py +58 -0
  23. nextmv/cli/cloud/app/exists.py +44 -0
  24. nextmv/cli/cloud/app/get.py +66 -0
  25. nextmv/cli/cloud/app/list.py +61 -0
  26. nextmv/cli/cloud/app/push.py +137 -0
  27. nextmv/cli/cloud/app/update.py +124 -0
  28. nextmv/cli/cloud/batch/__init__.py +29 -0
  29. nextmv/cli/cloud/batch/create.py +454 -0
  30. nextmv/cli/cloud/batch/delete.py +68 -0
  31. nextmv/cli/cloud/batch/get.py +104 -0
  32. nextmv/cli/cloud/batch/list.py +63 -0
  33. nextmv/cli/cloud/batch/metadata.py +66 -0
  34. nextmv/cli/cloud/batch/update.py +95 -0
  35. nextmv/cli/cloud/data/__init__.py +26 -0
  36. nextmv/cli/cloud/data/upload.py +162 -0
  37. nextmv/cli/cloud/ensemble/__init__.py +31 -0
  38. nextmv/cli/cloud/ensemble/create.py +414 -0
  39. nextmv/cli/cloud/ensemble/delete.py +67 -0
  40. nextmv/cli/cloud/ensemble/get.py +65 -0
  41. nextmv/cli/cloud/ensemble/update.py +103 -0
  42. nextmv/cli/cloud/input_set/__init__.py +30 -0
  43. nextmv/cli/cloud/input_set/create.py +170 -0
  44. nextmv/cli/cloud/input_set/get.py +63 -0
  45. nextmv/cli/cloud/input_set/list.py +63 -0
  46. nextmv/cli/cloud/input_set/update.py +123 -0
  47. nextmv/cli/cloud/instance/__init__.py +35 -0
  48. nextmv/cli/cloud/instance/create.py +290 -0
  49. nextmv/cli/cloud/instance/delete.py +62 -0
  50. nextmv/cli/cloud/instance/exists.py +39 -0
  51. nextmv/cli/cloud/instance/get.py +62 -0
  52. nextmv/cli/cloud/instance/list.py +60 -0
  53. nextmv/cli/cloud/instance/update.py +216 -0
  54. nextmv/cli/cloud/managed_input/__init__.py +31 -0
  55. nextmv/cli/cloud/managed_input/create.py +146 -0
  56. nextmv/cli/cloud/managed_input/delete.py +65 -0
  57. nextmv/cli/cloud/managed_input/get.py +63 -0
  58. nextmv/cli/cloud/managed_input/list.py +60 -0
  59. nextmv/cli/cloud/managed_input/update.py +97 -0
  60. nextmv/cli/cloud/run/__init__.py +37 -0
  61. nextmv/cli/cloud/run/cancel.py +37 -0
  62. nextmv/cli/cloud/run/create.py +530 -0
  63. nextmv/cli/cloud/run/get.py +199 -0
  64. nextmv/cli/cloud/run/input.py +86 -0
  65. nextmv/cli/cloud/run/list.py +80 -0
  66. nextmv/cli/cloud/run/logs.py +167 -0
  67. nextmv/cli/cloud/run/metadata.py +67 -0
  68. nextmv/cli/cloud/run/track.py +501 -0
  69. nextmv/cli/cloud/scenario/__init__.py +29 -0
  70. nextmv/cli/cloud/scenario/create.py +451 -0
  71. nextmv/cli/cloud/scenario/delete.py +65 -0
  72. nextmv/cli/cloud/scenario/get.py +102 -0
  73. nextmv/cli/cloud/scenario/list.py +63 -0
  74. nextmv/cli/cloud/scenario/metadata.py +67 -0
  75. nextmv/cli/cloud/scenario/update.py +93 -0
  76. nextmv/cli/cloud/secrets/__init__.py +33 -0
  77. nextmv/cli/cloud/secrets/create.py +206 -0
  78. nextmv/cli/cloud/secrets/delete.py +67 -0
  79. nextmv/cli/cloud/secrets/get.py +66 -0
  80. nextmv/cli/cloud/secrets/list.py +60 -0
  81. nextmv/cli/cloud/secrets/update.py +147 -0
  82. nextmv/cli/cloud/shadow/__init__.py +33 -0
  83. nextmv/cli/cloud/shadow/create.py +184 -0
  84. nextmv/cli/cloud/shadow/delete.py +68 -0
  85. nextmv/cli/cloud/shadow/get.py +61 -0
  86. nextmv/cli/cloud/shadow/list.py +63 -0
  87. nextmv/cli/cloud/shadow/metadata.py +66 -0
  88. nextmv/cli/cloud/shadow/start.py +43 -0
  89. nextmv/cli/cloud/shadow/stop.py +43 -0
  90. nextmv/cli/cloud/shadow/update.py +95 -0
  91. nextmv/cli/cloud/upload/__init__.py +22 -0
  92. nextmv/cli/cloud/upload/create.py +39 -0
  93. nextmv/cli/cloud/version/__init__.py +33 -0
  94. nextmv/cli/cloud/version/create.py +97 -0
  95. nextmv/cli/cloud/version/delete.py +62 -0
  96. nextmv/cli/cloud/version/exists.py +39 -0
  97. nextmv/cli/cloud/version/get.py +62 -0
  98. nextmv/cli/cloud/version/list.py +60 -0
  99. nextmv/cli/cloud/version/update.py +92 -0
  100. nextmv/cli/community/__init__.py +24 -0
  101. nextmv/cli/community/clone.py +270 -0
  102. nextmv/cli/community/list.py +265 -0
  103. nextmv/cli/configuration/__init__.py +23 -0
  104. nextmv/cli/configuration/config.py +195 -0
  105. nextmv/cli/configuration/create.py +94 -0
  106. nextmv/cli/configuration/delete.py +67 -0
  107. nextmv/cli/configuration/list.py +77 -0
  108. nextmv/cli/main.py +188 -0
  109. nextmv/cli/message.py +153 -0
  110. nextmv/cli/options.py +206 -0
  111. nextmv/cli/version.py +38 -0
  112. nextmv/cloud/__init__.py +71 -17
  113. nextmv/cloud/acceptance_test.py +757 -51
  114. nextmv/cloud/account.py +406 -17
  115. nextmv/cloud/application/__init__.py +957 -0
  116. nextmv/cloud/application/_acceptance.py +419 -0
  117. nextmv/cloud/application/_batch_scenario.py +860 -0
  118. nextmv/cloud/application/_ensemble.py +251 -0
  119. nextmv/cloud/application/_input_set.py +227 -0
  120. nextmv/cloud/application/_instance.py +289 -0
  121. nextmv/cloud/application/_managed_input.py +227 -0
  122. nextmv/cloud/application/_run.py +1393 -0
  123. nextmv/cloud/application/_secrets.py +294 -0
  124. nextmv/cloud/application/_shadow.py +314 -0
  125. nextmv/cloud/application/_utils.py +54 -0
  126. nextmv/cloud/application/_version.py +303 -0
  127. nextmv/cloud/assets.py +48 -0
  128. nextmv/cloud/batch_experiment.py +294 -33
  129. nextmv/cloud/client.py +307 -66
  130. nextmv/cloud/ensemble.py +247 -0
  131. nextmv/cloud/input_set.py +120 -2
  132. nextmv/cloud/instance.py +133 -8
  133. nextmv/cloud/integration.py +533 -0
  134. nextmv/cloud/package.py +168 -53
  135. nextmv/cloud/scenario.py +410 -0
  136. nextmv/cloud/secrets.py +234 -0
  137. nextmv/cloud/shadow.py +190 -0
  138. nextmv/cloud/url.py +73 -0
  139. nextmv/cloud/version.py +132 -4
  140. nextmv/default_app/.gitignore +1 -0
  141. nextmv/default_app/README.md +32 -0
  142. nextmv/default_app/app.yaml +12 -0
  143. nextmv/default_app/input.json +5 -0
  144. nextmv/default_app/main.py +37 -0
  145. nextmv/default_app/requirements.txt +2 -0
  146. nextmv/default_app/src/__init__.py +0 -0
  147. nextmv/default_app/src/visuals.py +36 -0
  148. nextmv/deprecated.py +47 -0
  149. nextmv/input.py +861 -90
  150. nextmv/local/__init__.py +5 -0
  151. nextmv/local/application.py +1251 -0
  152. nextmv/local/executor.py +1042 -0
  153. nextmv/local/geojson_handler.py +323 -0
  154. nextmv/local/local.py +97 -0
  155. nextmv/local/plotly_handler.py +61 -0
  156. nextmv/local/runner.py +274 -0
  157. nextmv/logger.py +80 -9
  158. nextmv/manifest.py +1466 -0
  159. nextmv/model.py +241 -66
  160. nextmv/options.py +708 -115
  161. nextmv/output.py +1301 -274
  162. nextmv/polling.py +325 -0
  163. nextmv/run.py +1702 -0
  164. nextmv/safe.py +145 -0
  165. nextmv/status.py +122 -0
  166. nextmv-1.0.0.dev2.dist-info/METADATA +311 -0
  167. nextmv-1.0.0.dev2.dist-info/RECORD +170 -0
  168. {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/WHEEL +1 -1
  169. nextmv-1.0.0.dev2.dist-info/entry_points.txt +2 -0
  170. nextmv/cloud/application.py +0 -1405
  171. nextmv/cloud/manifest.py +0 -234
  172. nextmv/cloud/status.py +0 -29
  173. nextmv-0.18.0.dist-info/METADATA +0 -770
  174. nextmv-0.18.0.dist-info/RECORD +0 -25
  175. {nextmv-0.18.0.dist-info → nextmv-1.0.0.dev2.dist-info}/licenses/LICENSE +0 -0
nextmv/run.py ADDED
@@ -0,0 +1,1702 @@
1
+ """
2
+ This module contains definitions for an app run.
3
+
4
+ Classes
5
+ -------
6
+ Metadata
7
+ Metadata of a run, whether it was successful or not.
8
+ RunInformation
9
+ Information of a run.
10
+ ErrorLog
11
+ Error log of a run, when it was not successful.
12
+ RunResult
13
+ Result of a run, whether it was successful or not.
14
+ RunLog
15
+ Log of a run.
16
+ FormatInput
17
+ Input format for a run configuration.
18
+ FormatOutput
19
+ Output format for a run configuration.
20
+ Format
21
+ Format for a run configuration.
22
+ RunType
23
+ The actual type of the run.
24
+ RunTypeConfiguration
25
+ Defines the configuration for the type of the run that is being executed
26
+ on an application.
27
+ RunQueuing
28
+ RunQueuing configuration for a run.
29
+ RunConfiguration
30
+ Configuration for an app run.
31
+ ExternalRunResult
32
+ Result of a run used to configure a new application run as an
33
+ external one.
34
+ TrackedRunStatus
35
+ The status of a tracked run.
36
+ TrackedRun
37
+ An external run that is tracked in the Nextmv platform.
38
+
39
+ Functions
40
+ ---------
41
+ run_duration(start, end)
42
+ Calculate the duration of a run in milliseconds.
43
+ """
44
+
45
+ from dataclasses import dataclass
46
+ from datetime import datetime
47
+ from enum import Enum
48
+ from typing import Any
49
+
50
+ from pydantic import AliasChoices, Field, field_validator
51
+
52
+ from nextmv._serialization import serialize_json
53
+ from nextmv.base_model import BaseModel
54
+ from nextmv.input import Input, InputFormat
55
+ from nextmv.output import Asset, Output, OutputFormat, Statistics
56
+ from nextmv.status import Status, StatusV2
57
+
58
+
59
+ def run_duration(start: datetime | float, end: datetime | float) -> int:
60
+ """
61
+ Calculate the duration of a run in milliseconds.
62
+
63
+ You can import the `run_duration` function directly from `nextmv`:
64
+
65
+ ```python
66
+ from nextmv import run_duration
67
+ ```
68
+
69
+ Parameters
70
+ ----------
71
+ start : datetime or float
72
+ The start time of the run. Can be a datetime object or a float
73
+ representing the start time in seconds since the epoch.
74
+ end : datetime or float
75
+ The end time of the run. Can be a datetime object or a float
76
+ representing the end time in seconds since the epoch.
77
+
78
+ Returns
79
+ -------
80
+ int
81
+ The duration of the run in milliseconds.
82
+
83
+ Raises
84
+ ------
85
+ ValueError
86
+ If the start time is after the end time.
87
+ TypeError
88
+ If start and end are not both datetime objects or both float numbers.
89
+
90
+ Examples
91
+ --------
92
+ >>> from datetime import datetime, timedelta
93
+ >>> start_dt = datetime(2023, 1, 1, 12, 0, 0)
94
+ >>> end_dt = datetime(2023, 1, 1, 12, 0, 1)
95
+ >>> run_duration(start_dt, end_dt)
96
+ 1000
97
+
98
+ >>> start_float = 1672574400.0 # Corresponds to 2023-01-01 12:00:00
99
+ >>> end_float = 1672574401.0 # Corresponds to 2023-01-01 12:00:01
100
+ >>> run_duration(start_float, end_float)
101
+ 1000
102
+ """
103
+ if isinstance(start, float) and isinstance(end, float):
104
+ if start > end:
105
+ raise ValueError("Start time must be before end time.")
106
+ return int(round((end - start) * 1000))
107
+
108
+ if isinstance(start, datetime) and isinstance(end, datetime):
109
+ if start > end:
110
+ raise ValueError("Start time must be before end time.")
111
+ return int(round((end - start).total_seconds() * 1000))
112
+
113
+ raise TypeError("Start and end must be either datetime or float.")
114
+
115
+
116
+ class FormatInput(BaseModel):
117
+ """
118
+ Input format for a run configuration.
119
+
120
+ You can import the `FormatInput` class directly from `nextmv`:
121
+
122
+ ```python
123
+ from nextmv import FormatInput
124
+ ```
125
+
126
+ Parameters
127
+ ----------
128
+ input_type : InputFormat, optional
129
+ Type of the input format. Defaults to `InputFormat.JSON`.
130
+
131
+ Examples
132
+ --------
133
+ >>> from nextmv import FormatInput, InputFormat
134
+ >>> format_input = FormatInput()
135
+ >>> format_input.input_type
136
+ <InputFormat.JSON: 'json'>
137
+
138
+ >>> format_input = FormatInput(input_type=InputFormat.TEXT)
139
+ >>> format_input.input_type
140
+ <InputFormat.TEXT: 'text'>
141
+ """
142
+
143
+ input_type: InputFormat = Field(
144
+ serialization_alias="type",
145
+ validation_alias=AliasChoices("type", "input_type"),
146
+ default=InputFormat.JSON,
147
+ )
148
+ """Type of the input format."""
149
+
150
+
151
+ class FormatOutput(BaseModel):
152
+ """
153
+ Output format for a run configuration.
154
+
155
+ You can import the `FormatOutput` class directly from `nextmv`:
156
+
157
+ ```python
158
+ from nextmv import FormatOutput
159
+ ```
160
+
161
+ Parameters
162
+ ----------
163
+ output_type : OutputFormat, optional
164
+ Type of the output format. Defaults to `OutputFormat.JSON`.
165
+
166
+ Examples
167
+ --------
168
+ >>> from nextmv import FormatOutput, OutputFormat
169
+ >>> format_output = FormatOutput()
170
+ >>> format_output.output_type
171
+ <OutputFormat.JSON: 'json'>
172
+
173
+ >>> format_output = FormatOutput(output_type=OutputFormat.CSV_ARCHIVE)
174
+ >>> format_output.output_type
175
+ <OutputFormat.CSV_ARCHIVE: 'csv_archive'>
176
+ """
177
+
178
+ output_type: OutputFormat = Field(
179
+ serialization_alias="type",
180
+ validation_alias=AliasChoices("type", "output_type"),
181
+ default=OutputFormat.JSON,
182
+ )
183
+ """Type of the output format."""
184
+
185
+
186
+ class Format(BaseModel):
187
+ """
188
+ Format for a run configuration.
189
+
190
+ You can import the `Format` class directly from `nextmv`:
191
+
192
+ ```python
193
+ from nextmv import Format
194
+ ```
195
+
196
+ Parameters
197
+ ----------
198
+ format_input : FormatInput
199
+ Input format for the run configuration.
200
+ format_output : FormatOutput, optional
201
+ Output format for the run configuration. Defaults to None.
202
+
203
+ Examples
204
+ --------
205
+ >>> from nextmv import Format, FormatInput, FormatOutput, InputFormat, OutputFormat
206
+ >>> format_config = Format(
207
+ ... format_input=FormatInput(input_type=InputFormat.JSON),
208
+ ... format_output=FormatOutput(output_type=OutputFormat.JSON)
209
+ ... )
210
+ >>> format_config.format_input.input_type
211
+ <InputFormat.JSON: 'json'>
212
+ >>> format_config.format_output.output_type
213
+ <OutputFormat.JSON: 'json'>
214
+ """
215
+
216
+ format_input: FormatInput = Field(
217
+ serialization_alias="input",
218
+ validation_alias=AliasChoices("input", "format_input"),
219
+ )
220
+ """Input format for the run configuration."""
221
+ format_output: FormatOutput | None = Field(
222
+ serialization_alias="output",
223
+ validation_alias=AliasChoices("output", "format_output"),
224
+ default=None,
225
+ )
226
+ """Output format for the run configuration."""
227
+
228
+
229
+ class RunType(str, Enum):
230
+ """
231
+ The actual type of the run.
232
+
233
+ You can import the `RunType` class directly from `nextmv`:
234
+
235
+ ```python
236
+ from nextmv import RunType
237
+ ```
238
+
239
+ Parameters
240
+ ----------
241
+ STANDARD : str
242
+ Standard run type.
243
+ EXTERNAL : str
244
+ External run type.
245
+ ENSEMBLE : str
246
+ Ensemble run type.
247
+
248
+ Examples
249
+ --------
250
+ >>> from nextmv import RunType
251
+ >>> run_type = RunType.STANDARD
252
+ >>> run_type
253
+ <RunType.STANDARD: 'standard'>
254
+ >>> run_type.value
255
+ 'standard'
256
+
257
+ >>> # Creating from string
258
+ >>> external_type = RunType("external")
259
+ >>> external_type
260
+ <RunType.EXTERNAL: 'external'>
261
+
262
+ >>> # All available types
263
+ >>> list(RunType)
264
+ [<RunType.STANDARD: 'standard'>, <RunType.EXTERNAL: 'external'>, <RunType.ENSEMBLE: 'ensemble'>]
265
+ """
266
+
267
+ STANDARD = "standard"
268
+ """Standard run type."""
269
+ EXTERNAL = "external"
270
+ """External run type."""
271
+ ENSEMBLE = "ensemble"
272
+ """Ensemble run type."""
273
+
274
+
275
+ class RunTypeConfiguration(BaseModel):
276
+ """
277
+ Defines the configuration for the type of the run that is being executed
278
+ on an application.
279
+
280
+ You can import the `RunTypeConfiguration` class directly from `nextmv`:
281
+
282
+ ```python
283
+ from nextmv import RunTypeConfiguration
284
+ ```
285
+
286
+ Parameters
287
+ ----------
288
+ run_type : RunType
289
+ Type of the run.
290
+ definition_id : str, optional
291
+ ID of the definition for the run type. Defaults to None.
292
+ reference_id : str, optional
293
+ ID of the reference for the run type. Defaults to None.
294
+
295
+ Examples
296
+ --------
297
+ >>> from nextmv import RunTypeConfiguration, RunType
298
+ >>> config = RunTypeConfiguration(run_type=RunType.STANDARD)
299
+ >>> config.run_type
300
+ <RunType.STANDARD: 'standard'>
301
+ >>> config.definition_id is None
302
+ True
303
+
304
+ >>> # External run with reference
305
+ >>> external_config = RunTypeConfiguration(
306
+ ... run_type=RunType.EXTERNAL,
307
+ ... reference_id="ref-12345"
308
+ ... )
309
+ >>> external_config.run_type
310
+ <RunType.EXTERNAL: 'external'>
311
+ >>> external_config.reference_id
312
+ 'ref-12345'
313
+
314
+ >>> # Ensemble run with definition
315
+ >>> ensemble_config = RunTypeConfiguration(
316
+ ... run_type=RunType.ENSEMBLE,
317
+ ... definition_id="def-67890"
318
+ ... )
319
+ >>> ensemble_config.run_type
320
+ <RunType.ENSEMBLE: 'ensemble'>
321
+ >>> ensemble_config.definition_id
322
+ 'def-67890'
323
+ """
324
+
325
+ run_type: RunType | None = Field(
326
+ serialization_alias="type",
327
+ validation_alias=AliasChoices("type", "run_type"),
328
+ default=None,
329
+ )
330
+ """Type of the run."""
331
+ definition_id: str | None = None
332
+ """ID of the definition for the run type."""
333
+ reference_id: str | None = None
334
+ """ID of the reference for the run type."""
335
+
336
+ @field_validator("run_type", mode="before")
337
+ @classmethod
338
+ def validate_run_type(cls, v):
339
+ """Convert empty string to None for run_type validation."""
340
+ if v == "":
341
+ return None
342
+ return v
343
+
344
+
345
+ class StatisticsIndicator(BaseModel):
346
+ """
347
+ Statistics indicator of a run.
348
+
349
+ You can import the `StatisticsIndicator` class directly from `nextmv`:
350
+
351
+ ```python
352
+ from nextmv import StatisticsIndicator
353
+ ```
354
+
355
+ Parameters
356
+ ----------
357
+ name : str
358
+ Name of the indicator.
359
+ value : Any
360
+ Value of the indicator.
361
+
362
+ Examples
363
+ --------
364
+ >>> from nextmv import StatisticsIndicator
365
+ >>> indicator = StatisticsIndicator(name="total_cost", value=1250.75)
366
+ >>> indicator.name
367
+ 'total_cost'
368
+ >>> indicator.value
369
+ 1250.75
370
+
371
+ >>> # Boolean indicator
372
+ >>> bool_indicator = StatisticsIndicator(name="optimal", value=True)
373
+ >>> bool_indicator.name
374
+ 'optimal'
375
+ >>> bool_indicator.value
376
+ True
377
+ """
378
+
379
+ name: str
380
+ """Name of the indicator."""
381
+ value: Any
382
+ """Value of the indicator."""
383
+
384
+
385
+ class RunInfoStatistics(BaseModel):
386
+ """
387
+ Statistics information for a run.
388
+
389
+ You can import the `RunInfoStatistics` class directly from `nextmv`:
390
+
391
+ ```python
392
+ from nextmv import RunInfoStatistics
393
+ ```
394
+
395
+ Parameters
396
+ ----------
397
+ status : str
398
+ Status of the statistics in the run.
399
+ error : str, optional
400
+ Error message if the statistics could not be retrieved. Defaults to None.
401
+ indicators : list[StatisticsIndicator], optional
402
+ List of statistics indicators. Defaults to None.
403
+
404
+ Examples
405
+ --------
406
+ >>> from nextmv import RunInfoStatistics, StatisticsIndicator
407
+ >>> indicators = [
408
+ ... StatisticsIndicator(name="total_cost", value=1250.75),
409
+ ... StatisticsIndicator(name="optimal", value=True)
410
+ ... ]
411
+ >>> stats = RunInfoStatistics(status="success", indicators=indicators)
412
+ >>> stats.status
413
+ 'success'
414
+ >>> len(stats.indicators)
415
+ 2
416
+
417
+ >>> # Statistics with error
418
+ >>> error_stats = RunInfoStatistics(
419
+ ... status="error",
420
+ ... error="Failed to calculate statistics"
421
+ ... )
422
+ >>> error_stats.status
423
+ 'error'
424
+ >>> error_stats.error
425
+ 'Failed to calculate statistics'
426
+ """
427
+
428
+ status: str
429
+ """Status of the statistics in the run."""
430
+
431
+ error: str | None = None
432
+ """Error message if the statistics could not be retrieved."""
433
+ indicators: list[StatisticsIndicator] | None = None
434
+ """List of statistics indicators."""
435
+
436
+
437
+ class OptionsSummaryItem(BaseModel):
438
+ """
439
+ Summary item for options used in a run.
440
+
441
+ You can import the `OptionsSummaryItem` class directly from `nextmv`:
442
+
443
+ ```python
444
+ from nextmv import OptionsSummaryItem
445
+ ```
446
+
447
+ Parameters
448
+ ----------
449
+ name : str
450
+ Name of the option.
451
+ value : Any
452
+ Value of the option.
453
+ source : str
454
+ Source of the option.
455
+
456
+ Examples
457
+ --------
458
+ >>> from nextmv import OptionsSummaryItem
459
+ >>> option = OptionsSummaryItem(
460
+ ... name="time_limit",
461
+ ... value=30,
462
+ ... source="config"
463
+ ... )
464
+ >>> option.name
465
+ 'time_limit'
466
+ >>> option.value
467
+ 30
468
+ >>> option.source
469
+ 'config'
470
+
471
+ >>> # Option from environment variable
472
+ >>> env_option = OptionsSummaryItem(
473
+ ... name="solver_type",
474
+ ... value="gurobi",
475
+ ... source="environment"
476
+ ... )
477
+ >>> env_option.source
478
+ 'environment'
479
+ """
480
+
481
+ name: str
482
+ """Name of the option."""
483
+ value: Any
484
+ """Value of the option."""
485
+ source: str
486
+ """Source of the option."""
487
+
488
+
489
+ class Run(BaseModel):
490
+ """
491
+ Information about a run in the Nextmv platform.
492
+
493
+ You can import the `Run` class directly from `nextmv`:
494
+
495
+ ```python
496
+ from nextmv import Run
497
+ ```
498
+
499
+ Parameters
500
+ ----------
501
+ id : str
502
+ ID of the run.
503
+ user_email : str
504
+ Email of the user who initiated the run.
505
+ name : str
506
+ Name of the run.
507
+ description : str
508
+ Description of the run.
509
+ created_at : datetime
510
+ Timestamp when the run was created.
511
+ application_id : str
512
+ ID of the application associated with the run.
513
+ application_instance_id : str
514
+ ID of the application instance associated with the run.
515
+ application_version_id : str
516
+ ID of the application version associated with the run.
517
+ run_type : RunTypeConfiguration
518
+ Configuration for the type of the run.
519
+ execution_class : str
520
+ Class name for the execution of a job.
521
+ runtime : str
522
+ Runtime environment for the run.
523
+ status : Status
524
+ Deprecated, use status_v2 instead.
525
+ status_v2 : StatusV2
526
+ Status of the run.
527
+ queuing_priority : int, optional
528
+ Priority of the run in the queue. Defaults to None.
529
+ queuing_disabled : bool, optional
530
+ Whether the run is disabled from queuing. Defaults to None.
531
+ experiment_id : str, optional
532
+ ID of the experiment associated with the run. Defaults to None.
533
+ statistics : RunInfoStatistics, optional
534
+ Statistics of the run. Defaults to None.
535
+ input_id : str, optional
536
+ ID of the input associated with the run. Defaults to None.
537
+ option_set : str, optional
538
+ ID of the option set associated with the run. Defaults to None.
539
+ options : dict[str, str], optional
540
+ Options associated with the run. Defaults to None.
541
+ request_options : dict[str, str], optional
542
+ Request options associated with the run. Defaults to None.
543
+ options_summary : list[OptionsSummaryItem], optional
544
+ Summary of options used in the run. Defaults to None.
545
+ scenario_id : str, optional
546
+ ID of the scenario associated with the run. Defaults to None.
547
+ repetition : int, optional
548
+ Repetition number of the run. Defaults to None.
549
+ input_set_id : str, optional
550
+ ID of the input set associated with the run. Defaults to None.
551
+
552
+ Examples
553
+ --------
554
+ >>> from nextmv import Run, RunTypeConfiguration, RunType, StatusV2
555
+ >>> from datetime import datetime
556
+ >>> run = Run(
557
+ ... id="run-12345",
558
+ ... user_email="user@example.com",
559
+ ... name="Test Run",
560
+ ... description="A test optimization run",
561
+ ... created_at=datetime.now(),
562
+ ... application_id="app-123",
563
+ ... application_instance_id="instance-456",
564
+ ... application_version_id="version-789",
565
+ ... run_type=RunTypeConfiguration(run_type=RunType.STANDARD),
566
+ ... execution_class="small",
567
+ ... runtime="python",
568
+ ... status_v2=StatusV2.SUCCEEDED
569
+ ... )
570
+ >>> run.id
571
+ 'run-12345'
572
+ >>> run.name
573
+ 'Test Run'
574
+ """
575
+
576
+ id: str
577
+ """ID of the run."""
578
+ user_email: str
579
+ """Email of the user who initiated the run."""
580
+ name: str
581
+ """Name of the run."""
582
+ description: str
583
+ """Description of the run."""
584
+ created_at: datetime
585
+ """Timestamp when the run was created."""
586
+ application_id: str
587
+ """ID of the application associated with the run."""
588
+ application_instance_id: str
589
+ """ID of the application instance associated with the run."""
590
+ application_version_id: str
591
+ """ID of the application version associated with the run."""
592
+ run_type: RunTypeConfiguration
593
+ """Configuration for the type of the run."""
594
+ execution_class: str
595
+ """Class name for the execution of a job."""
596
+ runtime: str
597
+ """Runtime environment for the run."""
598
+ status_v2: StatusV2
599
+ """Status of the run."""
600
+
601
+ status: Status | None = None
602
+ """Deprecated, use status_v2 instead."""
603
+ queuing_priority: int | None = None
604
+ """Priority of the run in the queue."""
605
+ queuing_disabled: bool | None = None
606
+ """Whether the run is disabled from queuing."""
607
+ experiment_id: str | None = None
608
+ """ID of the experiment associated with the run."""
609
+ statistics: RunInfoStatistics | None = None
610
+ """Statistics of the run."""
611
+ input_id: str | None = None
612
+ """ID of the input associated with the run."""
613
+ option_set: str | None = None
614
+ """ID of the option set associated with the run."""
615
+ options: dict[str, str] | None = None
616
+ """Options associated with the run."""
617
+ request_options: dict[str, str] | None = None
618
+ """Request options associated with the run."""
619
+ options_summary: list[OptionsSummaryItem] | None = None
620
+ """Summary of options used in the run."""
621
+ scenario_id: str | None = None
622
+ """ID of the scenario associated with the run."""
623
+ repetition: int | None = None
624
+ """Repetition number of the run."""
625
+ input_set_id: str | None = None
626
+ """ID of the input set associated with the run."""
627
+
628
+
629
+ class Metadata(BaseModel):
630
+ """
631
+ Metadata of a run, whether it was successful or not.
632
+
633
+ You can import the `Metadata` class directly from `nextmv`:
634
+
635
+ ```python
636
+ from nextmv import Metadata
637
+ ```
638
+
639
+ Parameters
640
+ ----------
641
+ application_id : str
642
+ ID of the application where the run was submitted to.
643
+ application_instance_id : str
644
+ ID of the instance where the run was submitted to.
645
+ application_version_id : str
646
+ ID of the version of the application where the run was submitted to.
647
+ created_at : datetime
648
+ Date and time when the run was created.
649
+ duration : float
650
+ Duration of the run in milliseconds.
651
+ error : str
652
+ Error message if the run failed.
653
+ input_size : float
654
+ Size of the input in bytes.
655
+ output_size : float
656
+ Size of the output in bytes.
657
+ format : Format
658
+ Format of the input and output of the run.
659
+ status : Status
660
+ Deprecated: use status_v2.
661
+ status_v2 : StatusV2
662
+ Status of the run.
663
+ """
664
+
665
+ application_id: str
666
+ """ID of the application where the run was submitted to."""
667
+ application_instance_id: str
668
+ """ID of the instance where the run was submitted to."""
669
+ application_version_id: str
670
+ """ID of the version of the application where the run was submitted to."""
671
+ created_at: datetime
672
+ """Date and time when the run was created."""
673
+ duration: float
674
+ """Duration of the run in milliseconds."""
675
+ error: str
676
+ """Error message if the run failed."""
677
+ input_size: float
678
+ """Size of the input in bytes."""
679
+ output_size: float
680
+ """Size of the output in bytes."""
681
+ format: Format
682
+ """Format of the input and output of the run."""
683
+ status_v2: StatusV2
684
+ """Status of the run."""
685
+ status: Status | None = None
686
+ """Deprecated: use status_v2."""
687
+ statistics: dict[str, Any] | None = None
688
+ """User defined statistics of the run."""
689
+
690
+ def run_is_finalized(self) -> bool:
691
+ """
692
+ Checks if the run has reached a finalized state.
693
+
694
+ Returns
695
+ -------
696
+ bool
697
+ True if the run status is one of `succeeded`, `failed`, or
698
+ `canceled`. False otherwise.
699
+ """
700
+
701
+ return self.status_v2 in {
702
+ StatusV2.succeeded,
703
+ StatusV2.failed,
704
+ StatusV2.canceled,
705
+ }
706
+
707
+
708
+ class SyncedRun(BaseModel):
709
+ """
710
+ Information about a run that has been synced to a remote application.
711
+
712
+ You can import the `SyncedRun` class directly from `nextmv`:
713
+
714
+ ```python
715
+ from nextmv import SyncedRun
716
+ ```
717
+
718
+ Parameters
719
+ ----------
720
+ run_id : str
721
+ ID of the synced remote run. When the `Application.sync` method is
722
+ used, this field marks the association between the local run (`id`) and
723
+ the remote run (`synced_run.id`).
724
+ synced_at : datetime
725
+ Timestamp when the run was synced with the remote run.
726
+ app_id : str
727
+ The ID of the remote application that the local run was synced to.
728
+ instance_id : Optional[str], optional
729
+ The instance of the remote application that the local run was synced
730
+ to. This field is optional and may be None. If it is not specified, it
731
+ indicates that the run was synced against the default instance of the
732
+ app. Defaults to None.
733
+ """
734
+
735
+ run_id: str
736
+ """
737
+ ID of the synced remote run. When the `Application.sync` method is used,
738
+ this field marks the association between the local run (`id`) and the
739
+ remote run (`synced_run.id`)
740
+ """
741
+ synced_at: datetime
742
+ """
743
+ Timestamp when the run was synced with the remote run.
744
+ """
745
+ app_id: str
746
+ """
747
+ The ID of the remote application that the local run was synced to.
748
+ """
749
+
750
+ instance_id: str | None = None
751
+ """
752
+ The instance of the remote application that the local run was synced to.
753
+ This field is optional and may be None. If it is not specified, it
754
+ indicates that the run was synced against the default instance of the app.
755
+ """
756
+
757
+
758
+ class RunInformation(BaseModel):
759
+ """
760
+ Information of a run.
761
+
762
+ You can import the `RunInformation` class directly from `nextmv`:
763
+
764
+ ```python
765
+ from nextmv import RunInformation
766
+ ```
767
+
768
+ Parameters
769
+ ----------
770
+ description : str
771
+ Description of the run.
772
+ id : str
773
+ ID of the run.
774
+ metadata : Metadata
775
+ Metadata of the run.
776
+ name : str
777
+ Name of the run.
778
+ user_email : str
779
+ Email of the user who submitted the run.
780
+ console_url : str, optional
781
+ URL to the run in the Nextmv console. Defaults to "".
782
+ """
783
+
784
+ description: str
785
+ """Description of the run."""
786
+ id: str
787
+ """ID of the run."""
788
+ metadata: Metadata
789
+ """Metadata of the run."""
790
+ name: str
791
+ """Name of the run."""
792
+ user_email: str
793
+ """Email of the user who submitted the run."""
794
+ console_url: str = Field(default="")
795
+ """
796
+ URL to the run in the Nextmv console.
797
+ """
798
+ synced_runs: list[SyncedRun] | None = None
799
+ """
800
+ List of synced runs associated with this run, if applicable. When the
801
+ `Application.sync` method is used, this field contains the associations
802
+ between the local run (`id`) and the remote runs (`synced_run.id`). This
803
+ field is None if the run was not created using `Application.sync` or if the
804
+ run has not been synced yet. It is possible to sync a single local run to
805
+ multiple remote runs. A remote run is identified by its application ID and
806
+ instance (if applicable). A local run cannot be synced to a remote run if
807
+ it is already present, this is, if there exists a record in the list with
808
+ the same application ID and instance. If there is not a repeated remote
809
+ run, a new record is added to the list.
810
+ """
811
+
812
+ def to_run(self) -> Run:
813
+ """
814
+ Transform this `RunInformation` instance into a `Run` instance.
815
+
816
+ This method maps all available attributes from the `RunInformation`
817
+ and its metadata to create a `Run` instance. Attributes that are not
818
+ available in RunInformation are set to None or appropriate defaults.
819
+
820
+ Returns
821
+ -------
822
+ Run
823
+ A Run instance with attributes populated from this RunInformation.
824
+
825
+ Examples
826
+ --------
827
+ >>> from nextmv import RunInformation, Metadata, Format, FormatInput, FormatOutput
828
+ >>> from nextmv import StatusV2, RunTypeConfiguration, RunType
829
+ >>> from datetime import datetime
830
+ >>> metadata = Metadata(
831
+ ... application_id="app-123",
832
+ ... application_instance_id="instance-456",
833
+ ... application_version_id="version-789",
834
+ ... created_at=datetime.now(),
835
+ ... duration=5000.0,
836
+ ... error="",
837
+ ... input_size=1024.0,
838
+ ... output_size=2048.0,
839
+ ... format=Format(
840
+ ... format_input=FormatInput(),
841
+ ... format_output=FormatOutput()
842
+ ... ),
843
+ ... status_v2=StatusV2.SUCCEEDED
844
+ ... )
845
+ >>> run_info = RunInformation(
846
+ ... id="run-123",
847
+ ... description="Test run",
848
+ ... name="Test",
849
+ ... user_email="user@example.com",
850
+ ... metadata=metadata
851
+ ... )
852
+ >>> run = run_info.to_run()
853
+ >>> run.id
854
+ 'run-123'
855
+ >>> run.application_id
856
+ 'app-123'
857
+ """
858
+ return Run(
859
+ id=self.id,
860
+ user_email=self.user_email,
861
+ name=self.name,
862
+ description=self.description,
863
+ created_at=self.metadata.created_at,
864
+ application_id=self.metadata.application_id,
865
+ application_instance_id=self.metadata.application_instance_id,
866
+ application_version_id=self.metadata.application_version_id,
867
+ run_type=RunTypeConfiguration(), # Default empty configuration
868
+ execution_class="", # Not available in RunInformation
869
+ runtime="", # Not available in RunInformation
870
+ status=self.metadata.status,
871
+ status_v2=self.metadata.status_v2,
872
+ # Optional fields that are not available in RunInformation
873
+ queuing_priority=None,
874
+ queuing_disabled=None,
875
+ experiment_id=None,
876
+ statistics=None,
877
+ input_id=None,
878
+ option_set=None,
879
+ options=None,
880
+ request_options=None,
881
+ options_summary=None,
882
+ scenario_id=None,
883
+ repetition=None,
884
+ input_set_id=None,
885
+ )
886
+
887
+ def add_synced_run(self, synced_run: SyncedRun) -> bool:
888
+ """
889
+ Add a synced run to the RunInformation.
890
+
891
+ This method adds a `SyncedRun` instance to the list of synced runs
892
+ associated with this `RunInformation`. If the list is None, it
893
+ initializes it first. If the run has already been synced, then it is
894
+ not added to the list. A run is already synced if there exists a record
895
+ in the list with the same application ID. This method returns True if
896
+ the synced run was added, and False otherwise.
897
+
898
+ Parameters
899
+ ----------
900
+ synced_run : SyncedRun
901
+ The SyncedRun instance to add.
902
+
903
+ Returns
904
+ -------
905
+ bool
906
+ True if the synced run was added, False if it was already present.
907
+ """
908
+
909
+ if self.synced_runs is None:
910
+ self.synced_runs = [synced_run]
911
+
912
+ return True
913
+
914
+ if synced_run.instance_id is None:
915
+ for existing_run in self.synced_runs:
916
+ if existing_run.app_id == synced_run.app_id:
917
+ return False
918
+ else:
919
+ for existing_run in self.synced_runs:
920
+ if existing_run.app_id == synced_run.app_id and existing_run.instance_id == synced_run.instance_id:
921
+ return False
922
+
923
+ self.synced_runs.append(synced_run)
924
+
925
+ return True
926
+
927
+ def is_synced(self, app_id: str, instance_id: str | None = None) -> tuple[SyncedRun, bool]:
928
+ """
929
+ Check if the run has been synced to a specific application and instance.
930
+
931
+ This method checks if there exists a `SyncedRun` in the list of synced
932
+ runs that matches the given application ID and optional instance ID.
933
+
934
+ Parameters
935
+ ----------
936
+ app_id : str
937
+ The application ID to check.
938
+ instance_id : Optional[str], optional
939
+ The instance ID to check. If None, only the application ID is
940
+ considered. Defaults to None.
941
+
942
+ Returns
943
+ -------
944
+ tuple[SyncedRun, bool]
945
+ A tuple containing the SyncedRun instance if found, and a boolean
946
+ indicating whether the run has been synced to the specified
947
+ application and instance.
948
+ """
949
+
950
+ if self.synced_runs is None:
951
+ return None, False
952
+
953
+ if instance_id is None:
954
+ for existing_run in self.synced_runs:
955
+ if existing_run.app_id == app_id:
956
+ return existing_run, True
957
+ else:
958
+ for existing_run in self.synced_runs:
959
+ if existing_run.app_id == app_id and existing_run.instance_id == instance_id:
960
+ return existing_run, True
961
+
962
+ return None, False
963
+
964
+
965
+ class ErrorLog(BaseModel):
966
+ """
967
+ Error log of a run, when it was not successful.
968
+
969
+ You can import the `ErrorLog` class directly from `nextmv`:
970
+
971
+ ```python
972
+ from nextmv import ErrorLog
973
+ ```
974
+
975
+ Parameters
976
+ ----------
977
+ error : str, optional
978
+ Error message. Defaults to None.
979
+ stdout : str, optional
980
+ Standard output. Defaults to None.
981
+ stderr : str, optional
982
+ Standard error. Defaults to None.
983
+ """
984
+
985
+ error: str | None = None
986
+ """Error message."""
987
+ stdout: str | None = None
988
+ """Standard output."""
989
+ stderr: str | None = None
990
+ """Standard error."""
991
+
992
+
993
+ class RunResult(RunInformation):
994
+ """
995
+ Result of a run, whether it was successful or not.
996
+
997
+ You can import the `RunResult` class directly from `nextmv`:
998
+
999
+ ```python
1000
+ from nextmv import RunResult
1001
+ ```
1002
+
1003
+ Parameters
1004
+ ----------
1005
+ error_log : ErrorLog, optional
1006
+ Error log of the run. Only available if the run failed. Defaults to
1007
+ None.
1008
+ output : dict[str, Any], optional
1009
+ Output of the run. Only available if the run succeeded. Defaults to
1010
+ None.
1011
+ """
1012
+
1013
+ error_log: ErrorLog | None = None
1014
+ """Error log of the run. Only available if the run failed."""
1015
+ output: dict[str, Any] | None = None
1016
+ """Output of the run. Only available if the run succeeded."""
1017
+
1018
+
1019
+ class RunLog(BaseModel):
1020
+ """
1021
+ Log of a run.
1022
+
1023
+ You can import the `RunLog` class directly from `nextmv`:
1024
+
1025
+ ```python
1026
+ from nextmv import RunLog
1027
+ ```
1028
+
1029
+ Parameters
1030
+ ----------
1031
+ log : str
1032
+ Log of the run.
1033
+
1034
+ Examples
1035
+ --------
1036
+ >>> from nextmv import RunLog
1037
+ >>> run_log = RunLog(log="Optimization completed successfully")
1038
+ >>> run_log.log
1039
+ 'Optimization completed successfully'
1040
+
1041
+ >>> # Multi-line log
1042
+ >>> multi_line_log = RunLog(log="Starting optimization\\nProcessing data\\nCompleted")
1043
+ >>> multi_line_log.log
1044
+ 'Starting optimization\\nProcessing data\\nCompleted'
1045
+ """
1046
+
1047
+ log: str
1048
+ """Log of the run."""
1049
+
1050
+
1051
+ class TimestampedRunLog(BaseModel):
1052
+ """
1053
+ Timestamped log entry of a run.
1054
+
1055
+ You can import the `TimestampedRunLog` class directly from `nextmv`:
1056
+
1057
+ ```python
1058
+ from nextmv import TimestampedRunLog
1059
+ ```
1060
+
1061
+ Parameters
1062
+ ----------
1063
+ timestamp : datetime
1064
+ Timestamp of the log entry.
1065
+ log : str
1066
+ Log message.
1067
+
1068
+ Examples
1069
+ --------
1070
+ >>> from nextmv import TimestampedRunLog
1071
+ >>> from datetime import datetime
1072
+ >>> log_entry = TimestampedRunLog(
1073
+ ... timestamp=datetime(2023, 1, 1, 12, 0, 0),
1074
+ ... log="Optimization started"
1075
+ ... )
1076
+ >>> log_entry.timestamp
1077
+ datetime.datetime(2023, 1, 1, 12, 0)
1078
+ >>> log_entry.log
1079
+ 'Optimization started'
1080
+ """
1081
+
1082
+ timestamp: datetime
1083
+ """Timestamp of the log entry."""
1084
+ log: str
1085
+ """Log message."""
1086
+
1087
+
1088
+ class RunQueuing(BaseModel):
1089
+ """
1090
+ RunQueuing configuration for a run.
1091
+
1092
+ You can import the `RunQueuing` class directly from `nextmv`:
1093
+
1094
+ ```python
1095
+ from nextmv import RunQueuing
1096
+ ```
1097
+
1098
+ Parameters
1099
+ ----------
1100
+ priority : int, optional
1101
+ Priority of the run in the queue. 1 is the highest priority, 9 is the
1102
+ lowest priority. Defaults to None.
1103
+ disabled : bool, optional
1104
+ Whether the run should be queued, or not. If True, the run will not be
1105
+ queued. If False, the run will be queued. Defaults to None.
1106
+
1107
+ Examples
1108
+ --------
1109
+ >>> from nextmv import RunQueuing
1110
+ >>> queuing = RunQueuing(priority=1, disabled=False)
1111
+ >>> queuing.priority
1112
+ 1
1113
+ >>> queuing.disabled
1114
+ False
1115
+
1116
+ >>> # High priority run
1117
+ >>> high_priority = RunQueuing(priority=1)
1118
+ >>> high_priority.priority
1119
+ 1
1120
+
1121
+ >>> # Disabled queuing
1122
+ >>> no_queue = RunQueuing(disabled=True)
1123
+ >>> no_queue.disabled
1124
+ True
1125
+ """
1126
+
1127
+ priority: int | None = None
1128
+ """
1129
+ Priority of the run in the queue. 1 is the highest priority, 9 is the
1130
+ lowest priority.
1131
+ """
1132
+ disabled: bool | None = None
1133
+ """
1134
+ Whether the run should be queued, or not. If True, the run will not be
1135
+ queued. If False, the run will be queued.
1136
+ """
1137
+
1138
+ def model_post_init(self, __context) -> None:
1139
+ """
1140
+ Validations done after parsing the model.
1141
+
1142
+ Raises
1143
+ ------
1144
+ ValueError
1145
+ If priority is not between 1 and 9, or if disabled is not a
1146
+ boolean value.
1147
+ """
1148
+
1149
+ if self.priority is not None and (self.priority < 1 or self.priority > 9):
1150
+ raise ValueError("Priority must be between 1 and 9.")
1151
+
1152
+ if self.disabled is not None and self.disabled not in {True, False}:
1153
+ raise ValueError("Disabled must be a boolean value.")
1154
+
1155
+
1156
+ class RunConfiguration(BaseModel):
1157
+ """
1158
+ Configuration for an app run.
1159
+
1160
+ You can import the `RunConfiguration` class directly from `nextmv`:
1161
+
1162
+ ```python
1163
+ from nextmv import RunConfiguration
1164
+ ```
1165
+
1166
+ Parameters
1167
+ ----------
1168
+ execution_class : str, optional
1169
+ Execution class for the instance. Defaults to None.
1170
+ format : Format, optional
1171
+ Format for the run configuration. Defaults to None.
1172
+ run_type : RunTypeConfiguration, optional
1173
+ Run type configuration for the run. Defaults to None.
1174
+ secrets_collection_id : str, optional
1175
+ ID of the secrets collection to use for the run. Defaults to None.
1176
+ queuing : RunQueuing, optional
1177
+ Queuing configuration for the run. Defaults to None.
1178
+ integration_id : str, optional
1179
+ ID of the integration to use for the run. Defaults to None.
1180
+
1181
+ Examples
1182
+ --------
1183
+ >>> from nextmv import RunConfiguration, RunQueuing
1184
+ >>> config = RunConfiguration(
1185
+ ... execution_class="large",
1186
+ ... queuing=RunQueuing(priority=1)
1187
+ ... )
1188
+ >>> config.execution_class
1189
+ 'large'
1190
+ >>> config.queuing.priority
1191
+ 1
1192
+
1193
+ >>> # Basic configuration
1194
+ >>> basic_config = RunConfiguration()
1195
+ >>> basic_config.format is None
1196
+ True
1197
+ """
1198
+
1199
+ execution_class: str | None = None
1200
+ """Execution class for the instance."""
1201
+ format: Format | None = None
1202
+ """Format for the run configuration."""
1203
+ run_type: RunTypeConfiguration | None = None
1204
+ """Run type configuration for the run."""
1205
+ secrets_collection_id: str | None = None
1206
+ """ID of the secrets collection to use for the run."""
1207
+ queuing: RunQueuing | None = None
1208
+ """Queuing configuration for the run."""
1209
+ integration_id: str | None = None
1210
+ """ID of the integration to use for the run."""
1211
+
1212
+ def model_post_init(self, __context) -> None:
1213
+ """
1214
+ Validations done after parsing the model.
1215
+
1216
+ Raises
1217
+ ------
1218
+ ValueError
1219
+ If execution_class is an empty string.
1220
+ """
1221
+
1222
+ if self.integration_id is None or self.integration_id == "":
1223
+ return
1224
+
1225
+ integration_val = "integration"
1226
+ if self.execution_class is not None and self.execution_class != "" and self.execution_class != integration_val:
1227
+ raise ValueError(f"When integration_id is set, execution_class must be `{integration_val}` or None.")
1228
+
1229
+ self.execution_class = integration_val
1230
+
1231
+ def resolve(
1232
+ self,
1233
+ input: Input | dict[str, Any] | BaseModel | str,
1234
+ dir_path: str | None = None,
1235
+ ) -> None:
1236
+ """
1237
+ Resolves the run configuration by modifying or setting the `format`,
1238
+ based on the type of input that is provided.
1239
+
1240
+ Parameters
1241
+ ----------
1242
+ input : Input or dict[str, Any] or BaseModel or str
1243
+ The input to use for resolving the run configuration.
1244
+ dir_path : str, optional
1245
+ The directory path where inputs can be loaded from.
1246
+
1247
+ Examples
1248
+ --------
1249
+ >>> from nextmv import RunConfiguration
1250
+ >>> config = RunConfiguration()
1251
+ >>> config.resolve({"key": "value"})
1252
+ >>> config.format.format_input.input_type
1253
+ <InputFormat.JSON: 'json'>
1254
+
1255
+ >>> config = RunConfiguration()
1256
+ >>> config.resolve("text input")
1257
+ >>> config.format.format_input.input_type
1258
+ <InputFormat.TEXT: 'text'>
1259
+
1260
+ >>> config = RunConfiguration()
1261
+ >>> config.resolve({}, dir_path="/path/to/files")
1262
+ >>> config.format.format_input.input_type
1263
+ <InputFormat.MULTI_FILE: 'multi_file'>
1264
+ """
1265
+
1266
+ # If the value is set by the user, do not change it.
1267
+ if self.format is not None:
1268
+ return
1269
+
1270
+ self.format = Format(
1271
+ format_input=FormatInput(input_type=InputFormat.JSON),
1272
+ format_output=FormatOutput(output_type=OutputFormat.JSON),
1273
+ )
1274
+
1275
+ if isinstance(input, dict):
1276
+ self.format.format_input.input_type = InputFormat.JSON
1277
+ elif isinstance(input, str):
1278
+ self.format.format_input.input_type = InputFormat.TEXT
1279
+ elif dir_path is not None and dir_path != "":
1280
+ # Kinda hard to detect if we should be working with CSV_ARCHIVE or
1281
+ # MULTI_FILE, so we default to MULTI_FILE.
1282
+ self.format.format_input.input_type = InputFormat.MULTI_FILE
1283
+ elif isinstance(input, Input):
1284
+ self.format.format_input.input_type = input.input_format
1285
+
1286
+ # As input and output are symmetric, we set the output according to the input
1287
+ # format.
1288
+ if self.format.format_input.input_type == InputFormat.JSON:
1289
+ self.format.format_output = FormatOutput(output_type=OutputFormat.JSON)
1290
+ elif self.format.format_input.input_type == InputFormat.TEXT: # Text still maps to json
1291
+ self.format.format_output = FormatOutput(output_type=OutputFormat.JSON)
1292
+ elif self.format.format_input.input_type == InputFormat.CSV_ARCHIVE:
1293
+ self.format.format_output = FormatOutput(output_type=OutputFormat.CSV_ARCHIVE)
1294
+ elif self.format.format_input.input_type == InputFormat.MULTI_FILE:
1295
+ self.format.format_output = FormatOutput(output_type=OutputFormat.MULTI_FILE)
1296
+ else:
1297
+ self.format.format_output = FormatOutput(output_type=OutputFormat.JSON)
1298
+
1299
+
1300
+ class ExternalRunResult(BaseModel):
1301
+ """
1302
+ Result of a run used to configure a new application run as an
1303
+ external one.
1304
+
1305
+ You can import the `ExternalRunResult` class directly from `nextmv`:
1306
+
1307
+ ```python
1308
+ from nextmv import ExternalRunResult
1309
+ ```
1310
+
1311
+ Parameters
1312
+ ----------
1313
+ output_upload_id : str, optional
1314
+ ID of the output upload. Defaults to None.
1315
+ error_upload_id : str, optional
1316
+ ID of the error upload. Defaults to None.
1317
+ status : str, optional
1318
+ Status of the run. Must be "succeeded" or "failed". Defaults to None.
1319
+ error_message : str, optional
1320
+ Error message of the run. Defaults to None.
1321
+ execution_duration : int, optional
1322
+ Duration of the run, in milliseconds. Defaults to None.
1323
+
1324
+ Examples
1325
+ --------
1326
+ >>> from nextmv import ExternalRunResult
1327
+ >>> # Successful external run
1328
+ >>> result = ExternalRunResult(
1329
+ ... output_upload_id="upload-12345",
1330
+ ... status="succeeded",
1331
+ ... execution_duration=5000
1332
+ ... )
1333
+ >>> result.status
1334
+ 'succeeded'
1335
+ >>> result.execution_duration
1336
+ 5000
1337
+
1338
+ >>> # Failed external run
1339
+ >>> failed_result = ExternalRunResult(
1340
+ ... error_upload_id="error-67890",
1341
+ ... status="failed",
1342
+ ... error_message="Optimization failed due to invalid constraints",
1343
+ ... execution_duration=2000
1344
+ ... )
1345
+ >>> failed_result.status
1346
+ 'failed'
1347
+ >>> failed_result.error_message
1348
+ 'Optimization failed due to invalid constraints'
1349
+ """
1350
+
1351
+ output_upload_id: str | None = None
1352
+ """ID of the output upload."""
1353
+ error_upload_id: str | None = None
1354
+ """ID of the error upload."""
1355
+ status: str | None = None
1356
+ """Status of the run."""
1357
+ error_message: str | None = None
1358
+ """Error message of the run."""
1359
+ execution_duration: int | None = None
1360
+ """Duration of the run, in milliseconds."""
1361
+ statistics_upload_id: str | None = None
1362
+ """
1363
+ ID of the statistics upload. Use this field when working with `CSV_ARCHIVE`
1364
+ or `MULTI_FILE` output formats.
1365
+ """
1366
+ assets_upload_id: str | None = None
1367
+ """
1368
+ ID of the assets upload. Use this field when working with `CSV_ARCHIVE`
1369
+ or `MULTI_FILE` output formats.
1370
+ """
1371
+
1372
+ def model_post_init(self, __context) -> None:
1373
+ """
1374
+ Validations done after parsing the model.
1375
+
1376
+ Raises
1377
+ ------
1378
+ ValueError
1379
+ If the status value is not "succeeded" or "failed".
1380
+ """
1381
+
1382
+ valid_statuses = {"succeeded", "failed"}
1383
+ if self.status is not None and self.status not in valid_statuses:
1384
+ raise ValueError("Invalid status value, must be one of: " + ", ".join(valid_statuses))
1385
+
1386
+
1387
+ class TrackedRunStatus(str, Enum):
1388
+ """
1389
+ The status of a tracked run.
1390
+
1391
+ You can import the `TrackedRunStatus` class directly from `nextmv`:
1392
+
1393
+ ```python
1394
+ from nextmv import TrackedRunStatus
1395
+ ```
1396
+
1397
+ Parameters
1398
+ ----------
1399
+ SUCCEEDED : str
1400
+ The run succeeded.
1401
+ FAILED : str
1402
+ The run failed.
1403
+
1404
+ Examples
1405
+ --------
1406
+ >>> from nextmv import TrackedRunStatus
1407
+ >>> status = TrackedRunStatus.SUCCEEDED
1408
+ >>> status
1409
+ <TrackedRunStatus.SUCCEEDED: 'succeeded'>
1410
+ >>> status.value
1411
+ 'succeeded'
1412
+
1413
+ >>> # Creating from string
1414
+ >>> failed_status = TrackedRunStatus("failed")
1415
+ >>> failed_status
1416
+ <TrackedRunStatus.FAILED: 'failed'>
1417
+
1418
+ >>> # All available statuses
1419
+ >>> list(TrackedRunStatus)
1420
+ [<TrackedRunStatus.SUCCEEDED: 'succeeded'>, <TrackedRunStatus.FAILED: 'failed'>]
1421
+ """
1422
+
1423
+ SUCCEEDED = "succeeded"
1424
+ """The run succeeded."""
1425
+ FAILED = "failed"
1426
+ """The run failed."""
1427
+
1428
+
1429
+ @dataclass
1430
+ class TrackedRun:
1431
+ """
1432
+ An external run that is tracked in the Nextmv platform.
1433
+
1434
+ You can import the `TrackedRun` class directly from `nextmv`:
1435
+
1436
+ ```python
1437
+ from nextmv import TrackedRun
1438
+ ```
1439
+
1440
+ Parameters
1441
+ ----------
1442
+ status : TrackedRunStatus
1443
+ The status of the run being tracked. This field is required.
1444
+ input : Input or dict[str, Any] or str, optional
1445
+ The input of the run being tracked. Please note that if the input
1446
+ format is JSON, then the input data must be JSON serializable. If both
1447
+ `input` and `input_dir_path` are specified, the `input` is ignored, and
1448
+ the files in the directory are used instead. Defaults to None.
1449
+ output : Output or dict[str, Any] or str, optional
1450
+ The output of the run being tracked. Please note that if the output
1451
+ format is JSON, then the output data must be JSON serializable. If both
1452
+ `output` and `output_dir_path` are specified, the `output` is ignored,
1453
+ and the files in the directory are used instead. Defaults to None.
1454
+ duration : int, optional
1455
+ The duration of the run being tracked, in milliseconds. This field is
1456
+ optional. Defaults to None.
1457
+ error : str, optional
1458
+ An error message if the run failed. You should only specify this if the
1459
+ run failed (the `status` is `TrackedRunStatus.FAILED`), otherwise an
1460
+ exception will be raised. This field is optional. Defaults to None.
1461
+ logs : str or list[str], optional
1462
+ The logs of the run being tracked. If the logs are provided as a list,
1463
+ each element of the list is a line in the log. This field is optional.
1464
+ Defaults to None.
1465
+ name : str, optional
1466
+ Optional name for the run being tracked. Defaults to None.
1467
+ description : str, optional
1468
+ Optional description for the run being tracked. Defaults to None.
1469
+ input_dir_path : str, optional
1470
+ Path to a directory containing input files. If specified, the calling
1471
+ function will package the files in the directory into a tar file and
1472
+ upload it as a large input. This is useful for non-JSON input formats,
1473
+ such as when working with `CSV_ARCHIVE` or `MULTI_FILE`. If both
1474
+ `input` and `input_dir_path` are specified, the `input` is ignored, and
1475
+ the files in the directory are used instead. Defaults to None.
1476
+ output_dir_path : str, optional
1477
+ Path to a directory containing output files. If specified, the calling
1478
+ function will package the files in the directory into a tar file and
1479
+ upload it as a large output. This is useful for non-JSON output
1480
+ formats, such as when working with `CSV_ARCHIVE` or `MULTI_FILE`. If
1481
+ both `output` and `output_dir_path` are specified, the `output` is
1482
+ ignored, and the files are saved in the directory instead. Defaults to
1483
+ None.
1484
+ statistics : Statistics or dict[str, Any], optional
1485
+ Statistics of the run being tracked. Only use this field if you want to
1486
+ track statistics for `CSV_ARCHIVE` or `MULTI_FILE` output formats. If
1487
+ you are working with `JSON` or `TEXT` output formats, this field will
1488
+ be ignored, as the statistics are extracted directly from the `output`.
1489
+ This field is optional. Defaults to None.
1490
+ assets : list[Asset or dict[str, Any]], optional
1491
+ Assets associated with the run being tracked. Only use this field if
1492
+ you want to track assets for `CSV_ARCHIVE` or `MULTI_FILE` output
1493
+ formats. If you are working with `JSON` or `TEXT` output formats, this
1494
+ field will be ignored, as the assets are extracted directly from the
1495
+ `output`. This field is optional. Defaults to None.
1496
+
1497
+ Examples
1498
+ --------
1499
+ >>> from nextmv import TrackedRun, TrackedRunStatus
1500
+ >>> # Successful run
1501
+ >>> run = TrackedRun(
1502
+ ... status=TrackedRunStatus.SUCCEEDED,
1503
+ ... input={"vehicles": 5, "locations": 10},
1504
+ ... output={"routes": [{"stops": [1, 2, 3]}]},
1505
+ ... duration=5000,
1506
+ ... name="test-run",
1507
+ ... description="A test optimization run"
1508
+ ... )
1509
+ >>> run.status
1510
+ <TrackedRunStatus.SUCCEEDED: 'succeeded'>
1511
+ >>> run.duration
1512
+ 5000
1513
+
1514
+ >>> # Failed run with error
1515
+ >>> failed_run = TrackedRun(
1516
+ ... status=TrackedRunStatus.FAILED,
1517
+ ... input={"vehicles": 0},
1518
+ ... error="No vehicles available for routing",
1519
+ ... duration=1000,
1520
+ ... logs=["Starting optimization", "Error: No vehicles found"]
1521
+ ... )
1522
+ >>> failed_run.status
1523
+ <TrackedRunStatus.FAILED: 'failed'>
1524
+ >>> failed_run.error
1525
+ 'No vehicles available for routing'
1526
+
1527
+ >>> # Run with directory-based input/output
1528
+ >>> dir_run = TrackedRun(
1529
+ ... status=TrackedRunStatus.SUCCEEDED,
1530
+ ... input_dir_path="/path/to/input/files",
1531
+ ... output_dir_path="/path/to/output/files",
1532
+ ... duration=10000
1533
+ ... )
1534
+ >>> dir_run.input_dir_path
1535
+ '/path/to/input/files'
1536
+
1537
+ Raises
1538
+ ------
1539
+ ValueError
1540
+ If the status value is invalid, if an error message is provided for a
1541
+ successful run, or if input/output formats are not JSON or input/output
1542
+ dicts are not JSON serializable.
1543
+ """
1544
+
1545
+ status: TrackedRunStatus
1546
+ """The status of the run being tracked"""
1547
+
1548
+ input: Input | dict[str, Any] | str | None = None
1549
+ """
1550
+ The input of the run being tracked. Please note that if the input
1551
+ format is JSON, then the input data must be JSON serializable. If both
1552
+ `input` and `input_dir_path` are specified, the `input` is ignored, and
1553
+ the files in the directory are used instead.
1554
+ """
1555
+ output: Output | dict[str, Any] | str | None = None
1556
+ """
1557
+ The output of the run being tracked. Please note that if the output
1558
+ format is JSON, then the output data must be JSON serializable. If both
1559
+ `output` and `output_dir_path` are specified, the `output` is ignored, and
1560
+ the files in the directory are used instead.
1561
+ """
1562
+ duration: int | None = None
1563
+ """The duration of the run being tracked, in milliseconds."""
1564
+ error: str | None = None
1565
+ """An error message if the run failed. You should only specify this if the
1566
+ run failed, otherwise an exception will be raised."""
1567
+ logs: str | list[str] | None = None
1568
+ """The logs of the run being tracked. If a list, each element is a line in
1569
+ the log."""
1570
+ name: str | None = None
1571
+ """
1572
+ Optional name for the run being tracked.
1573
+ """
1574
+ description: str | None = None
1575
+ """
1576
+ Optional description for the run being tracked.
1577
+ """
1578
+ input_dir_path: str | None = None
1579
+ """
1580
+ Path to a directory containing input files. If specified, the calling
1581
+ function will package the files in the directory into a tar file and upload
1582
+ it as a large input. This is useful for non-JSON input formats, such as
1583
+ when working with `CSV_ARCHIVE` or `MULTI_FILE`. If both `input` and
1584
+ `input_dir_path` are specified, the `input` is ignored, and the files in
1585
+ the directory are used instead.
1586
+ """
1587
+ output_dir_path: str | None = None
1588
+ """
1589
+ Path to a directory containing output files. If specified, the calling
1590
+ function will package the files in the directory into a tar file and upload
1591
+ it as a large output. This is useful for non-JSON output formats, such as
1592
+ when working with `CSV_ARCHIVE` or `MULTI_FILE`. If both `output` and
1593
+ `output_dir_path` are specified, the `output` is ignored, and the files
1594
+ are saved in the directory instead.
1595
+ """
1596
+ statistics: Statistics | dict[str, Any] | None = None
1597
+ """
1598
+ Statistics of the run being tracked. Only use this field if you want to
1599
+ track statistics for `CSV_ARCHIVE` or `MULTI_FILE` output formats. If you
1600
+ are working with `JSON` or `TEXT` output formats, this field will be
1601
+ ignored, as the statistics are extracted directly from the `output`.
1602
+ """
1603
+ assets: list[Asset | dict[str, Any]] | None = None
1604
+ """
1605
+ Assets associated with the run being tracked. Only use this field if you
1606
+ want to track assets for `CSV_ARCHIVE` or `MULTI_FILE` output formats.
1607
+ If you are working with `JSON` or `TEXT` output formats, this field will
1608
+ be ignored, as the assets are extracted directly from the `output`.
1609
+ """
1610
+
1611
+ def __post_init__(self): # noqa: C901
1612
+ """
1613
+ Validations done after parsing the model.
1614
+
1615
+ Raises
1616
+ ------
1617
+ ValueError
1618
+ If the status value is invalid, if an error message is provided for
1619
+ a successful run, or if input/output formats are not JSON or
1620
+ input/output dicts are not JSON serializable.
1621
+ """
1622
+
1623
+ valid_statuses = {TrackedRunStatus.SUCCEEDED, TrackedRunStatus.FAILED}
1624
+ if self.status not in valid_statuses:
1625
+ raise ValueError("Invalid status value, must be one of: " + ", ".join(valid_statuses))
1626
+
1627
+ if self.error is not None and self.error != "" and self.status != TrackedRunStatus.FAILED:
1628
+ raise ValueError("Error message must be empty if the run succeeded.")
1629
+
1630
+ if isinstance(self.input, Input):
1631
+ try:
1632
+ _ = serialize_json(self.input.data)
1633
+ except (TypeError, OverflowError) as e:
1634
+ raise ValueError("Input.data is not JSON serializable") from e
1635
+ elif isinstance(self.input, dict):
1636
+ try:
1637
+ _ = serialize_json(self.input)
1638
+ except (TypeError, OverflowError) as e:
1639
+ raise ValueError("Input is dict[str, Any] but it is not JSON serializable") from e
1640
+
1641
+ if isinstance(self.output, Output):
1642
+ try:
1643
+ _ = serialize_json(self.output.solution)
1644
+ except (TypeError, OverflowError) as e:
1645
+ raise ValueError("`Output.solution` is not JSON serializable") from e
1646
+ elif isinstance(self.output, dict):
1647
+ try:
1648
+ _ = serialize_json(self.output)
1649
+ except (TypeError, OverflowError) as e:
1650
+ raise ValueError("Output is dict[str, Any] but it is not JSON serializable") from e
1651
+
1652
+ def logs_text(self) -> str:
1653
+ """
1654
+ Returns the logs as a single string.
1655
+
1656
+ Each log entry is separated by a newline character.
1657
+
1658
+ Returns
1659
+ -------
1660
+ str
1661
+ The logs as a single string. If no logs are present, an empty
1662
+ string is returned.
1663
+
1664
+ Examples
1665
+ --------
1666
+ >>> from nextmv import TrackedRun, TrackedRunStatus
1667
+ >>> run = TrackedRun(
1668
+ ... status=TrackedRunStatus.SUCCEEDED,
1669
+ ... logs=["Starting optimization", "Processing data", "Optimization complete"]
1670
+ ... )
1671
+ >>> run.logs_text()
1672
+ 'Starting optimization\\nProcessing data\\nOptimization complete'
1673
+
1674
+ >>> # Single string log
1675
+ >>> run_with_string_log = TrackedRun(
1676
+ ... status=TrackedRunStatus.SUCCEEDED,
1677
+ ... logs="Single log entry"
1678
+ ... )
1679
+ >>> run_with_string_log.logs_text()
1680
+ 'Single log entry'
1681
+
1682
+ >>> # No logs
1683
+ >>> run_no_logs = TrackedRun(status=TrackedRunStatus.SUCCEEDED)
1684
+ >>> run_no_logs.logs_text()
1685
+ ''
1686
+
1687
+ Raises
1688
+ ------
1689
+ TypeError
1690
+ If `self.logs` is not a string or a list of strings.
1691
+ """
1692
+
1693
+ if self.logs is None:
1694
+ return ""
1695
+
1696
+ if isinstance(self.logs, str):
1697
+ return self.logs
1698
+
1699
+ if isinstance(self.logs, list):
1700
+ return "\\n".join(self.logs)
1701
+
1702
+ raise TypeError("Logs must be a string or a list of strings.")