nextmv 0.10.3.dev0__py3-none-any.whl → 0.35.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.
Files changed (61) hide show
  1. nextmv/__about__.py +1 -1
  2. nextmv/__entrypoint__.py +39 -0
  3. nextmv/__init__.py +57 -0
  4. nextmv/_serialization.py +96 -0
  5. nextmv/base_model.py +79 -9
  6. nextmv/cloud/__init__.py +71 -10
  7. nextmv/cloud/acceptance_test.py +888 -17
  8. nextmv/cloud/account.py +154 -10
  9. nextmv/cloud/application.py +3644 -437
  10. nextmv/cloud/batch_experiment.py +292 -33
  11. nextmv/cloud/client.py +354 -53
  12. nextmv/cloud/ensemble.py +247 -0
  13. nextmv/cloud/input_set.py +121 -4
  14. nextmv/cloud/instance.py +125 -0
  15. nextmv/cloud/package.py +474 -0
  16. nextmv/cloud/scenario.py +410 -0
  17. nextmv/cloud/secrets.py +234 -0
  18. nextmv/cloud/url.py +73 -0
  19. nextmv/cloud/version.py +174 -0
  20. nextmv/default_app/.gitignore +1 -0
  21. nextmv/default_app/README.md +32 -0
  22. nextmv/default_app/app.yaml +12 -0
  23. nextmv/default_app/input.json +5 -0
  24. nextmv/default_app/main.py +37 -0
  25. nextmv/default_app/requirements.txt +2 -0
  26. nextmv/default_app/src/__init__.py +0 -0
  27. nextmv/default_app/src/main.py +37 -0
  28. nextmv/default_app/src/visuals.py +36 -0
  29. nextmv/deprecated.py +47 -0
  30. nextmv/input.py +883 -78
  31. nextmv/local/__init__.py +5 -0
  32. nextmv/local/application.py +1263 -0
  33. nextmv/local/executor.py +1040 -0
  34. nextmv/local/geojson_handler.py +323 -0
  35. nextmv/local/local.py +97 -0
  36. nextmv/local/plotly_handler.py +61 -0
  37. nextmv/local/runner.py +274 -0
  38. nextmv/logger.py +80 -9
  39. nextmv/manifest.py +1472 -0
  40. nextmv/model.py +431 -0
  41. nextmv/options.py +968 -78
  42. nextmv/output.py +1363 -231
  43. nextmv/polling.py +287 -0
  44. nextmv/run.py +1623 -0
  45. nextmv/safe.py +145 -0
  46. nextmv/status.py +122 -0
  47. {nextmv-0.10.3.dev0.dist-info → nextmv-0.35.0.dist-info}/METADATA +51 -288
  48. nextmv-0.35.0.dist-info/RECORD +50 -0
  49. {nextmv-0.10.3.dev0.dist-info → nextmv-0.35.0.dist-info}/WHEEL +1 -1
  50. nextmv/cloud/status.py +0 -29
  51. nextmv/nextroute/__init__.py +0 -2
  52. nextmv/nextroute/check/__init__.py +0 -26
  53. nextmv/nextroute/check/schema.py +0 -141
  54. nextmv/nextroute/schema/__init__.py +0 -19
  55. nextmv/nextroute/schema/input.py +0 -52
  56. nextmv/nextroute/schema/location.py +0 -13
  57. nextmv/nextroute/schema/output.py +0 -136
  58. nextmv/nextroute/schema/stop.py +0 -61
  59. nextmv/nextroute/schema/vehicle.py +0 -68
  60. nextmv-0.10.3.dev0.dist-info/RECORD +0 -28
  61. {nextmv-0.10.3.dev0.dist-info → nextmv-0.35.0.dist-info}/licenses/LICENSE +0 -0
nextmv/run.py ADDED
@@ -0,0 +1,1623 @@
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
+
691
+ class SyncedRun(BaseModel):
692
+ """
693
+ Information about a run that has been synced to a remote application.
694
+
695
+ You can import the `SyncedRun` class directly from `nextmv`:
696
+
697
+ ```python
698
+ from nextmv import SyncedRun
699
+ ```
700
+
701
+ Parameters
702
+ ----------
703
+ run_id : str
704
+ ID of the synced remote run. When the `Application.sync` method is
705
+ used, this field marks the association between the local run (`id`) and
706
+ the remote run (`synced_run.id`).
707
+ synced_at : datetime
708
+ Timestamp when the run was synced with the remote run.
709
+ app_id : str
710
+ The ID of the remote application that the local run was synced to.
711
+ instance_id : Optional[str], optional
712
+ The instance of the remote application that the local run was synced
713
+ to. This field is optional and may be None. If it is not specified, it
714
+ indicates that the run was synced against the default instance of the
715
+ app. Defaults to None.
716
+ """
717
+
718
+ run_id: str
719
+ """
720
+ ID of the synced remote run. When the `Application.sync` method is used,
721
+ this field marks the association between the local run (`id`) and the
722
+ remote run (`synced_run.id`)
723
+ """
724
+ synced_at: datetime
725
+ """
726
+ Timestamp when the run was synced with the remote run.
727
+ """
728
+ app_id: str
729
+ """
730
+ The ID of the remote application that the local run was synced to.
731
+ """
732
+
733
+ instance_id: str | None = None
734
+ """
735
+ The instance of the remote application that the local run was synced to.
736
+ This field is optional and may be None. If it is not specified, it
737
+ indicates that the run was synced against the default instance of the app.
738
+ """
739
+
740
+
741
+ class RunInformation(BaseModel):
742
+ """
743
+ Information of a run.
744
+
745
+ You can import the `RunInformation` class directly from `nextmv`:
746
+
747
+ ```python
748
+ from nextmv import RunInformation
749
+ ```
750
+
751
+ Parameters
752
+ ----------
753
+ description : str
754
+ Description of the run.
755
+ id : str
756
+ ID of the run.
757
+ metadata : Metadata
758
+ Metadata of the run.
759
+ name : str
760
+ Name of the run.
761
+ user_email : str
762
+ Email of the user who submitted the run.
763
+ console_url : str, optional
764
+ URL to the run in the Nextmv console. Defaults to "".
765
+ """
766
+
767
+ description: str
768
+ """Description of the run."""
769
+ id: str
770
+ """ID of the run."""
771
+ metadata: Metadata
772
+ """Metadata of the run."""
773
+ name: str
774
+ """Name of the run."""
775
+ user_email: str
776
+ """Email of the user who submitted the run."""
777
+ console_url: str = Field(default="")
778
+ """
779
+ URL to the run in the Nextmv console.
780
+ """
781
+ synced_runs: list[SyncedRun] | None = None
782
+ """
783
+ List of synced runs associated with this run, if applicable. When the
784
+ `Application.sync` method is used, this field contains the associations
785
+ between the local run (`id`) and the remote runs (`synced_run.id`). This
786
+ field is None if the run was not created using `Application.sync` or if the
787
+ run has not been synced yet. It is possible to sync a single local run to
788
+ multiple remote runs. A remote run is identified by its application ID and
789
+ instance (if applicable). A local run cannot be synced to a remote run if
790
+ it is already present, this is, if there exists a record in the list with
791
+ the same application ID and instance. If there is not a repeated remote
792
+ run, a new record is added to the list.
793
+ """
794
+
795
+ def to_run(self) -> Run:
796
+ """
797
+ Transform this `RunInformation` instance into a `Run` instance.
798
+
799
+ This method maps all available attributes from the `RunInformation`
800
+ and its metadata to create a `Run` instance. Attributes that are not
801
+ available in RunInformation are set to None or appropriate defaults.
802
+
803
+ Returns
804
+ -------
805
+ Run
806
+ A Run instance with attributes populated from this RunInformation.
807
+
808
+ Examples
809
+ --------
810
+ >>> from nextmv import RunInformation, Metadata, Format, FormatInput, FormatOutput
811
+ >>> from nextmv import StatusV2, RunTypeConfiguration, RunType
812
+ >>> from datetime import datetime
813
+ >>> metadata = Metadata(
814
+ ... application_id="app-123",
815
+ ... application_instance_id="instance-456",
816
+ ... application_version_id="version-789",
817
+ ... created_at=datetime.now(),
818
+ ... duration=5000.0,
819
+ ... error="",
820
+ ... input_size=1024.0,
821
+ ... output_size=2048.0,
822
+ ... format=Format(
823
+ ... format_input=FormatInput(),
824
+ ... format_output=FormatOutput()
825
+ ... ),
826
+ ... status_v2=StatusV2.SUCCEEDED
827
+ ... )
828
+ >>> run_info = RunInformation(
829
+ ... id="run-123",
830
+ ... description="Test run",
831
+ ... name="Test",
832
+ ... user_email="user@example.com",
833
+ ... metadata=metadata
834
+ ... )
835
+ >>> run = run_info.to_run()
836
+ >>> run.id
837
+ 'run-123'
838
+ >>> run.application_id
839
+ 'app-123'
840
+ """
841
+ return Run(
842
+ id=self.id,
843
+ user_email=self.user_email,
844
+ name=self.name,
845
+ description=self.description,
846
+ created_at=self.metadata.created_at,
847
+ application_id=self.metadata.application_id,
848
+ application_instance_id=self.metadata.application_instance_id,
849
+ application_version_id=self.metadata.application_version_id,
850
+ run_type=RunTypeConfiguration(), # Default empty configuration
851
+ execution_class="", # Not available in RunInformation
852
+ runtime="", # Not available in RunInformation
853
+ status=self.metadata.status,
854
+ status_v2=self.metadata.status_v2,
855
+ # Optional fields that are not available in RunInformation
856
+ queuing_priority=None,
857
+ queuing_disabled=None,
858
+ experiment_id=None,
859
+ statistics=None,
860
+ input_id=None,
861
+ option_set=None,
862
+ options=None,
863
+ request_options=None,
864
+ options_summary=None,
865
+ scenario_id=None,
866
+ repetition=None,
867
+ input_set_id=None,
868
+ )
869
+
870
+ def add_synced_run(self, synced_run: SyncedRun) -> bool:
871
+ """
872
+ Add a synced run to the RunInformation.
873
+
874
+ This method adds a `SyncedRun` instance to the list of synced runs
875
+ associated with this `RunInformation`. If the list is None, it
876
+ initializes it first. If the run has already been synced, then it is
877
+ not added to the list. A run is already synced if there exists a record
878
+ in the list with the same application ID. This method returns True if
879
+ the synced run was added, and False otherwise.
880
+
881
+ Parameters
882
+ ----------
883
+ synced_run : SyncedRun
884
+ The SyncedRun instance to add.
885
+
886
+ Returns
887
+ -------
888
+ bool
889
+ True if the synced run was added, False if it was already present.
890
+ """
891
+
892
+ if self.synced_runs is None:
893
+ self.synced_runs = [synced_run]
894
+
895
+ return True
896
+
897
+ if synced_run.instance_id is None:
898
+ for existing_run in self.synced_runs:
899
+ if existing_run.app_id == synced_run.app_id:
900
+ return False
901
+ else:
902
+ for existing_run in self.synced_runs:
903
+ if existing_run.app_id == synced_run.app_id and existing_run.instance_id == synced_run.instance_id:
904
+ return False
905
+
906
+ self.synced_runs.append(synced_run)
907
+
908
+ return True
909
+
910
+ def is_synced(self, app_id: str, instance_id: str | None = None) -> tuple[SyncedRun, bool]:
911
+ """
912
+ Check if the run has been synced to a specific application and instance.
913
+
914
+ This method checks if there exists a `SyncedRun` in the list of synced
915
+ runs that matches the given application ID and optional instance ID.
916
+
917
+ Parameters
918
+ ----------
919
+ app_id : str
920
+ The application ID to check.
921
+ instance_id : Optional[str], optional
922
+ The instance ID to check. If None, only the application ID is
923
+ considered. Defaults to None.
924
+
925
+ Returns
926
+ -------
927
+ tuple[SyncedRun, bool]
928
+ A tuple containing the SyncedRun instance if found, and a boolean
929
+ indicating whether the run has been synced to the specified
930
+ application and instance.
931
+ """
932
+
933
+ if self.synced_runs is None:
934
+ return None, False
935
+
936
+ if instance_id is None:
937
+ for existing_run in self.synced_runs:
938
+ if existing_run.app_id == app_id:
939
+ return existing_run, True
940
+ else:
941
+ for existing_run in self.synced_runs:
942
+ if existing_run.app_id == app_id and existing_run.instance_id == instance_id:
943
+ return existing_run, True
944
+
945
+ return None, False
946
+
947
+
948
+ class ErrorLog(BaseModel):
949
+ """
950
+ Error log of a run, when it was not successful.
951
+
952
+ You can import the `ErrorLog` class directly from `nextmv`:
953
+
954
+ ```python
955
+ from nextmv import ErrorLog
956
+ ```
957
+
958
+ Parameters
959
+ ----------
960
+ error : str, optional
961
+ Error message. Defaults to None.
962
+ stdout : str, optional
963
+ Standard output. Defaults to None.
964
+ stderr : str, optional
965
+ Standard error. Defaults to None.
966
+ """
967
+
968
+ error: str | None = None
969
+ """Error message."""
970
+ stdout: str | None = None
971
+ """Standard output."""
972
+ stderr: str | None = None
973
+ """Standard error."""
974
+
975
+
976
+ class RunResult(RunInformation):
977
+ """
978
+ Result of a run, whether it was successful or not.
979
+
980
+ You can import the `RunResult` class directly from `nextmv`:
981
+
982
+ ```python
983
+ from nextmv import RunResult
984
+ ```
985
+
986
+ Parameters
987
+ ----------
988
+ error_log : ErrorLog, optional
989
+ Error log of the run. Only available if the run failed. Defaults to
990
+ None.
991
+ output : dict[str, Any], optional
992
+ Output of the run. Only available if the run succeeded. Defaults to
993
+ None.
994
+ """
995
+
996
+ error_log: ErrorLog | None = None
997
+ """Error log of the run. Only available if the run failed."""
998
+ output: dict[str, Any] | None = None
999
+ """Output of the run. Only available if the run succeeded."""
1000
+
1001
+
1002
+ class RunLog(BaseModel):
1003
+ """
1004
+ Log of a run.
1005
+
1006
+ You can import the `RunLog` class directly from `nextmv`:
1007
+
1008
+ ```python
1009
+ from nextmv import RunLog
1010
+ ```
1011
+
1012
+ Parameters
1013
+ ----------
1014
+ log : str
1015
+ Log of the run.
1016
+
1017
+ Examples
1018
+ --------
1019
+ >>> from nextmv import RunLog
1020
+ >>> run_log = RunLog(log="Optimization completed successfully")
1021
+ >>> run_log.log
1022
+ 'Optimization completed successfully'
1023
+
1024
+ >>> # Multi-line log
1025
+ >>> multi_line_log = RunLog(log="Starting optimization\\nProcessing data\\nCompleted")
1026
+ >>> multi_line_log.log
1027
+ 'Starting optimization\\nProcessing data\\nCompleted'
1028
+ """
1029
+
1030
+ log: str
1031
+ """Log of the run."""
1032
+
1033
+
1034
+ class RunQueuing(BaseModel):
1035
+ """
1036
+ RunQueuing configuration for a run.
1037
+
1038
+ You can import the `RunQueuing` class directly from `nextmv`:
1039
+
1040
+ ```python
1041
+ from nextmv import RunQueuing
1042
+ ```
1043
+
1044
+ Parameters
1045
+ ----------
1046
+ priority : int, optional
1047
+ Priority of the run in the queue. 1 is the highest priority, 9 is the
1048
+ lowest priority. Defaults to None.
1049
+ disabled : bool, optional
1050
+ Whether the run should be queued, or not. If True, the run will not be
1051
+ queued. If False, the run will be queued. Defaults to None.
1052
+
1053
+ Examples
1054
+ --------
1055
+ >>> from nextmv import RunQueuing
1056
+ >>> queuing = RunQueuing(priority=1, disabled=False)
1057
+ >>> queuing.priority
1058
+ 1
1059
+ >>> queuing.disabled
1060
+ False
1061
+
1062
+ >>> # High priority run
1063
+ >>> high_priority = RunQueuing(priority=1)
1064
+ >>> high_priority.priority
1065
+ 1
1066
+
1067
+ >>> # Disabled queuing
1068
+ >>> no_queue = RunQueuing(disabled=True)
1069
+ >>> no_queue.disabled
1070
+ True
1071
+ """
1072
+
1073
+ priority: int | None = None
1074
+ """
1075
+ Priority of the run in the queue. 1 is the highest priority, 9 is the
1076
+ lowest priority.
1077
+ """
1078
+ disabled: bool | None = None
1079
+ """
1080
+ Whether the run should be queued, or not. If True, the run will not be
1081
+ queued. If False, the run will be queued.
1082
+ """
1083
+
1084
+ def __post_init_post_parse__(self):
1085
+ """
1086
+ Validations done after parsing the model.
1087
+
1088
+ Raises
1089
+ ------
1090
+ ValueError
1091
+ If priority is not between 1 and 9, or if disabled is not a
1092
+ boolean value.
1093
+ """
1094
+
1095
+ if self.priority is not None and (self.priority < 1 or self.priority > 9):
1096
+ raise ValueError("Priority must be between 1 and 9.")
1097
+
1098
+ if self.disabled is not None and self.disabled not in {True, False}:
1099
+ raise ValueError("Disabled must be a boolean value.")
1100
+
1101
+
1102
+ class RunConfiguration(BaseModel):
1103
+ """
1104
+ Configuration for an app run.
1105
+
1106
+ You can import the `RunConfiguration` class directly from `nextmv`:
1107
+
1108
+ ```python
1109
+ from nextmv import RunConfiguration
1110
+ ```
1111
+
1112
+ Parameters
1113
+ ----------
1114
+ execution_class : str, optional
1115
+ Execution class for the instance. Defaults to None.
1116
+ format : Format, optional
1117
+ Format for the run configuration. Defaults to None.
1118
+ run_type : RunTypeConfiguration, optional
1119
+ Run type configuration for the run. Defaults to None.
1120
+ secrets_collection_id : str, optional
1121
+ ID of the secrets collection to use for the run. Defaults to None.
1122
+ queuing : RunQueuing, optional
1123
+ Queuing configuration for the run. Defaults to None.
1124
+
1125
+ Examples
1126
+ --------
1127
+ >>> from nextmv import RunConfiguration, RunQueuing
1128
+ >>> config = RunConfiguration(
1129
+ ... execution_class="large",
1130
+ ... queuing=RunQueuing(priority=1)
1131
+ ... )
1132
+ >>> config.execution_class
1133
+ 'large'
1134
+ >>> config.queuing.priority
1135
+ 1
1136
+
1137
+ >>> # Basic configuration
1138
+ >>> basic_config = RunConfiguration()
1139
+ >>> basic_config.format is None
1140
+ True
1141
+ """
1142
+
1143
+ execution_class: str | None = None
1144
+ """Execution class for the instance."""
1145
+ format: Format | None = None
1146
+ """Format for the run configuration."""
1147
+ run_type: RunTypeConfiguration | None = None
1148
+ """Run type configuration for the run."""
1149
+ secrets_collection_id: str | None = None
1150
+ """ID of the secrets collection to use for the run."""
1151
+ queuing: RunQueuing | None = None
1152
+ """Queuing configuration for the run."""
1153
+
1154
+ def resolve(
1155
+ self,
1156
+ input: Input | dict[str, Any] | BaseModel | str,
1157
+ dir_path: str | None = None,
1158
+ ) -> None:
1159
+ """
1160
+ Resolves the run configuration by modifying or setting the `format`,
1161
+ based on the type of input that is provided.
1162
+
1163
+ Parameters
1164
+ ----------
1165
+ input : Input or dict[str, Any] or BaseModel or str
1166
+ The input to use for resolving the run configuration.
1167
+ dir_path : str, optional
1168
+ The directory path where inputs can be loaded from.
1169
+
1170
+ Examples
1171
+ --------
1172
+ >>> from nextmv import RunConfiguration
1173
+ >>> config = RunConfiguration()
1174
+ >>> config.resolve({"key": "value"})
1175
+ >>> config.format.format_input.input_type
1176
+ <InputFormat.JSON: 'json'>
1177
+
1178
+ >>> config = RunConfiguration()
1179
+ >>> config.resolve("text input")
1180
+ >>> config.format.format_input.input_type
1181
+ <InputFormat.TEXT: 'text'>
1182
+
1183
+ >>> config = RunConfiguration()
1184
+ >>> config.resolve({}, dir_path="/path/to/files")
1185
+ >>> config.format.format_input.input_type
1186
+ <InputFormat.MULTI_FILE: 'multi_file'>
1187
+ """
1188
+
1189
+ # If the value is set by the user, do not change it.
1190
+ if self.format is not None:
1191
+ return
1192
+
1193
+ self.format = Format(
1194
+ format_input=FormatInput(input_type=InputFormat.JSON),
1195
+ format_output=FormatOutput(output_type=OutputFormat.JSON),
1196
+ )
1197
+
1198
+ if isinstance(input, dict):
1199
+ self.format.format_input.input_type = InputFormat.JSON
1200
+ elif isinstance(input, str):
1201
+ self.format.format_input.input_type = InputFormat.TEXT
1202
+ elif dir_path is not None and dir_path != "":
1203
+ # Kinda hard to detect if we should be working with CSV_ARCHIVE or
1204
+ # MULTI_FILE, so we default to MULTI_FILE.
1205
+ self.format.format_input.input_type = InputFormat.MULTI_FILE
1206
+ elif isinstance(input, Input):
1207
+ self.format.format_input.input_type = input.input_format
1208
+
1209
+ # As input and output are symmetric, we set the output according to the input
1210
+ # format.
1211
+ if self.format.format_input.input_type == InputFormat.JSON:
1212
+ self.format.format_output = FormatOutput(output_type=OutputFormat.JSON)
1213
+ elif self.format.format_input.input_type == InputFormat.TEXT: # Text still maps to json
1214
+ self.format.format_output = FormatOutput(output_type=OutputFormat.JSON)
1215
+ elif self.format.format_input.input_type == InputFormat.CSV_ARCHIVE:
1216
+ self.format.format_output = FormatOutput(output_type=OutputFormat.CSV_ARCHIVE)
1217
+ elif self.format.format_input.input_type == InputFormat.MULTI_FILE:
1218
+ self.format.format_output = FormatOutput(output_type=OutputFormat.MULTI_FILE)
1219
+ else:
1220
+ self.format.format_output = FormatOutput(output_type=OutputFormat.JSON)
1221
+
1222
+
1223
+ class ExternalRunResult(BaseModel):
1224
+ """
1225
+ Result of a run used to configure a new application run as an
1226
+ external one.
1227
+
1228
+ You can import the `ExternalRunResult` class directly from `nextmv`:
1229
+
1230
+ ```python
1231
+ from nextmv import ExternalRunResult
1232
+ ```
1233
+
1234
+ Parameters
1235
+ ----------
1236
+ output_upload_id : str, optional
1237
+ ID of the output upload. Defaults to None.
1238
+ error_upload_id : str, optional
1239
+ ID of the error upload. Defaults to None.
1240
+ status : str, optional
1241
+ Status of the run. Must be "succeeded" or "failed". Defaults to None.
1242
+ error_message : str, optional
1243
+ Error message of the run. Defaults to None.
1244
+ execution_duration : int, optional
1245
+ Duration of the run, in milliseconds. Defaults to None.
1246
+
1247
+ Examples
1248
+ --------
1249
+ >>> from nextmv import ExternalRunResult
1250
+ >>> # Successful external run
1251
+ >>> result = ExternalRunResult(
1252
+ ... output_upload_id="upload-12345",
1253
+ ... status="succeeded",
1254
+ ... execution_duration=5000
1255
+ ... )
1256
+ >>> result.status
1257
+ 'succeeded'
1258
+ >>> result.execution_duration
1259
+ 5000
1260
+
1261
+ >>> # Failed external run
1262
+ >>> failed_result = ExternalRunResult(
1263
+ ... error_upload_id="error-67890",
1264
+ ... status="failed",
1265
+ ... error_message="Optimization failed due to invalid constraints",
1266
+ ... execution_duration=2000
1267
+ ... )
1268
+ >>> failed_result.status
1269
+ 'failed'
1270
+ >>> failed_result.error_message
1271
+ 'Optimization failed due to invalid constraints'
1272
+ """
1273
+
1274
+ output_upload_id: str | None = None
1275
+ """ID of the output upload."""
1276
+ error_upload_id: str | None = None
1277
+ """ID of the error upload."""
1278
+ status: str | None = None
1279
+ """Status of the run."""
1280
+ error_message: str | None = None
1281
+ """Error message of the run."""
1282
+ execution_duration: int | None = None
1283
+ """Duration of the run, in milliseconds."""
1284
+ statistics_upload_id: str | None = None
1285
+ """
1286
+ ID of the statistics upload. Use this field when working with `CSV_ARCHIVE`
1287
+ or `MULTI_FILE` output formats.
1288
+ """
1289
+ assets_upload_id: str | None = None
1290
+ """
1291
+ ID of the assets upload. Use this field when working with `CSV_ARCHIVE`
1292
+ or `MULTI_FILE` output formats.
1293
+ """
1294
+
1295
+ def __post_init_post_parse__(self):
1296
+ """
1297
+ Validations done after parsing the model.
1298
+
1299
+ Raises
1300
+ ------
1301
+ ValueError
1302
+ If the status value is not "succeeded" or "failed".
1303
+ """
1304
+
1305
+ valid_statuses = {"succeeded", "failed"}
1306
+ if self.status is not None and self.status not in valid_statuses:
1307
+ raise ValueError("Invalid status value, must be one of: " + ", ".join(valid_statuses))
1308
+
1309
+
1310
+ class TrackedRunStatus(str, Enum):
1311
+ """
1312
+ The status of a tracked run.
1313
+
1314
+ You can import the `TrackedRunStatus` class directly from `nextmv`:
1315
+
1316
+ ```python
1317
+ from nextmv import TrackedRunStatus
1318
+ ```
1319
+
1320
+ Parameters
1321
+ ----------
1322
+ SUCCEEDED : str
1323
+ The run succeeded.
1324
+ FAILED : str
1325
+ The run failed.
1326
+
1327
+ Examples
1328
+ --------
1329
+ >>> from nextmv import TrackedRunStatus
1330
+ >>> status = TrackedRunStatus.SUCCEEDED
1331
+ >>> status
1332
+ <TrackedRunStatus.SUCCEEDED: 'succeeded'>
1333
+ >>> status.value
1334
+ 'succeeded'
1335
+
1336
+ >>> # Creating from string
1337
+ >>> failed_status = TrackedRunStatus("failed")
1338
+ >>> failed_status
1339
+ <TrackedRunStatus.FAILED: 'failed'>
1340
+
1341
+ >>> # All available statuses
1342
+ >>> list(TrackedRunStatus)
1343
+ [<TrackedRunStatus.SUCCEEDED: 'succeeded'>, <TrackedRunStatus.FAILED: 'failed'>]
1344
+ """
1345
+
1346
+ SUCCEEDED = "succeeded"
1347
+ """The run succeeded."""
1348
+ FAILED = "failed"
1349
+ """The run failed."""
1350
+
1351
+
1352
+ @dataclass
1353
+ class TrackedRun:
1354
+ """
1355
+ An external run that is tracked in the Nextmv platform.
1356
+
1357
+ You can import the `TrackedRun` class directly from `nextmv`:
1358
+
1359
+ ```python
1360
+ from nextmv import TrackedRun
1361
+ ```
1362
+
1363
+ Parameters
1364
+ ----------
1365
+ status : TrackedRunStatus
1366
+ The status of the run being tracked. This field is required.
1367
+ input : Input or dict[str, Any] or str, optional
1368
+ The input of the run being tracked. Please note that if the input
1369
+ format is JSON, then the input data must be JSON serializable. If both
1370
+ `input` and `input_dir_path` are specified, the `input` is ignored, and
1371
+ the files in the directory are used instead. Defaults to None.
1372
+ output : Output or dict[str, Any] or str, optional
1373
+ The output of the run being tracked. Please note that if the output
1374
+ format is JSON, then the output data must be JSON serializable. If both
1375
+ `output` and `output_dir_path` are specified, the `output` is ignored, and
1376
+ the files in the directory are used instead. Defaults to None.
1377
+ duration : int, optional
1378
+ The duration of the run being tracked, in milliseconds. This field is
1379
+ optional. Defaults to None.
1380
+ error : str, optional
1381
+ An error message if the run failed. You should only specify this if the
1382
+ run failed (the `status` is `TrackedRunStatus.FAILED`), otherwise an
1383
+ exception will be raised. This field is optional. Defaults to None.
1384
+ logs : list[str], optional
1385
+ The logs of the run being tracked. Each element of the list is a line in
1386
+ the log. This field is optional. Defaults to None.
1387
+ name : str, optional
1388
+ Optional name for the run being tracked. Defaults to None.
1389
+ description : str, optional
1390
+ Optional description for the run being tracked. Defaults to None.
1391
+ input_dir_path : str, optional
1392
+ Path to a directory containing input files. If specified, the calling
1393
+ function will package the files in the directory into a tar file and upload
1394
+ it as a large input. This is useful for non-JSON input formats, such as
1395
+ when working with `CSV_ARCHIVE` or `MULTI_FILE`. If both `input` and
1396
+ `input_dir_path` are specified, the `input` is ignored, and the files in
1397
+ the directory are used instead. Defaults to None.
1398
+ output_dir_path : str, optional
1399
+ Path to a directory containing output files. If specified, the calling
1400
+ function will package the files in the directory into a tar file and upload
1401
+ it as a large output. This is useful for non-JSON output formats, such as
1402
+ when working with `CSV_ARCHIVE` or `MULTI_FILE`. If both `output` and
1403
+ `output_dir_path` are specified, the `output` is ignored, and the files
1404
+ are saved in the directory instead. Defaults to None.
1405
+ statistics : Statistics or dict[str, Any], optional
1406
+ Statistics of the run being tracked. Only use this field if you want to
1407
+ track statistics for `CSV_ARCHIVE` or `MULTI_FILE` output formats. If you
1408
+ are working with `JSON` or `TEXT` output formats, this field will be
1409
+ ignored, as the statistics are extracted directly from the `output`.
1410
+ This field is optional. Defaults to None.
1411
+ assets : list[Asset or dict[str, Any]], optional
1412
+ Assets associated with the run being tracked. Only use this field if you
1413
+ want to track assets for `CSV_ARCHIVE` or `MULTI_FILE` output formats.
1414
+ If you are working with `JSON` or `TEXT` output formats, this field will
1415
+ be ignored, as the assets are extracted directly from the `output`.
1416
+ This field is optional. Defaults to None.
1417
+
1418
+ Examples
1419
+ --------
1420
+ >>> from nextmv import TrackedRun, TrackedRunStatus
1421
+ >>> # Successful run
1422
+ >>> run = TrackedRun(
1423
+ ... status=TrackedRunStatus.SUCCEEDED,
1424
+ ... input={"vehicles": 5, "locations": 10},
1425
+ ... output={"routes": [{"stops": [1, 2, 3]}]},
1426
+ ... duration=5000,
1427
+ ... name="test-run",
1428
+ ... description="A test optimization run"
1429
+ ... )
1430
+ >>> run.status
1431
+ <TrackedRunStatus.SUCCEEDED: 'succeeded'>
1432
+ >>> run.duration
1433
+ 5000
1434
+
1435
+ >>> # Failed run with error
1436
+ >>> failed_run = TrackedRun(
1437
+ ... status=TrackedRunStatus.FAILED,
1438
+ ... input={"vehicles": 0},
1439
+ ... error="No vehicles available for routing",
1440
+ ... duration=1000,
1441
+ ... logs=["Starting optimization", "Error: No vehicles found"]
1442
+ ... )
1443
+ >>> failed_run.status
1444
+ <TrackedRunStatus.FAILED: 'failed'>
1445
+ >>> failed_run.error
1446
+ 'No vehicles available for routing'
1447
+
1448
+ >>> # Run with directory-based input/output
1449
+ >>> dir_run = TrackedRun(
1450
+ ... status=TrackedRunStatus.SUCCEEDED,
1451
+ ... input_dir_path="/path/to/input/files",
1452
+ ... output_dir_path="/path/to/output/files",
1453
+ ... duration=10000
1454
+ ... )
1455
+ >>> dir_run.input_dir_path
1456
+ '/path/to/input/files'
1457
+
1458
+ Raises
1459
+ ------
1460
+ ValueError
1461
+ If the status value is invalid, if an error message is provided for a
1462
+ successful run, or if input/output formats are not JSON or
1463
+ input/output dicts are not JSON serializable.
1464
+ """
1465
+
1466
+ status: TrackedRunStatus
1467
+ """The status of the run being tracked"""
1468
+
1469
+ input: Input | dict[str, Any] | str | None = None
1470
+ """
1471
+ The input of the run being tracked. Please note that if the input
1472
+ format is JSON, then the input data must be JSON serializable. If both
1473
+ `input` and `input_dir_path` are specified, the `input` is ignored, and
1474
+ the files in the directory are used instead.
1475
+ """
1476
+ output: Output | dict[str, Any] | str | None = None
1477
+ """
1478
+ The output of the run being tracked. Please note that if the output
1479
+ format is JSON, then the output data must be JSON serializable. If both
1480
+ `output` and `output_dir_path` are specified, the `output` is ignored, and
1481
+ the files in the directory are used instead.
1482
+ """
1483
+ duration: int | None = None
1484
+ """The duration of the run being tracked, in milliseconds."""
1485
+ error: str | None = None
1486
+ """An error message if the run failed. You should only specify this if the
1487
+ run failed, otherwise an exception will be raised."""
1488
+ logs: list[str] | None = None
1489
+ """The logs of the run being tracked. Each element of the list is a line in
1490
+ the log."""
1491
+ name: str | None = None
1492
+ """
1493
+ Optional name for the run being tracked.
1494
+ """
1495
+ description: str | None = None
1496
+ """
1497
+ Optional description for the run being tracked.
1498
+ """
1499
+ input_dir_path: str | None = None
1500
+ """
1501
+ Path to a directory containing input files. If specified, the calling
1502
+ function will package the files in the directory into a tar file and upload
1503
+ it as a large input. This is useful for non-JSON input formats, such as
1504
+ when working with `CSV_ARCHIVE` or `MULTI_FILE`. If both `input` and
1505
+ `input_dir_path` are specified, the `input` is ignored, and the files in
1506
+ the directory are used instead.
1507
+ """
1508
+ output_dir_path: str | None = None
1509
+ """
1510
+ Path to a directory containing output files. If specified, the calling
1511
+ function will package the files in the directory into a tar file and upload
1512
+ it as a large output. This is useful for non-JSON output formats, such as
1513
+ when working with `CSV_ARCHIVE` or `MULTI_FILE`. If both `output` and
1514
+ `output_dir_path` are specified, the `output` is ignored, and the files
1515
+ are saved in the directory instead.
1516
+ """
1517
+ statistics: Statistics | dict[str, Any] | None = None
1518
+ """
1519
+ Statistics of the run being tracked. Only use this field if you want to
1520
+ track statistics for `CSV_ARCHIVE` or `MULTI_FILE` output formats. If you
1521
+ are working with `JSON` or `TEXT` output formats, this field will be
1522
+ ignored, as the statistics are extracted directly from the `output`.
1523
+ """
1524
+ assets: list[Asset | dict[str, Any]] | None = None
1525
+ """
1526
+ Assets associated with the run being tracked. Only use this field if you
1527
+ want to track assets for `CSV_ARCHIVE` or `MULTI_FILE` output formats.
1528
+ If you are working with `JSON` or `TEXT` output formats, this field will
1529
+ be ignored, as the assets are extracted directly from the `output`.
1530
+ """
1531
+
1532
+ def __post_init__(self): # noqa: C901
1533
+ """
1534
+ Validations done after parsing the model.
1535
+
1536
+ Raises
1537
+ ------
1538
+ ValueError
1539
+ If the status value is invalid, if an error message is provided for
1540
+ a successful run, or if input/output formats are not JSON or
1541
+ input/output dicts are not JSON serializable.
1542
+ """
1543
+
1544
+ valid_statuses = {TrackedRunStatus.SUCCEEDED, TrackedRunStatus.FAILED}
1545
+ if self.status not in valid_statuses:
1546
+ raise ValueError("Invalid status value, must be one of: " + ", ".join(valid_statuses))
1547
+
1548
+ if self.error is not None and self.error != "" and self.status != TrackedRunStatus.FAILED:
1549
+ raise ValueError("Error message must be empty if the run succeeded.")
1550
+
1551
+ if isinstance(self.input, Input):
1552
+ try:
1553
+ _ = serialize_json(self.input.data)
1554
+ except (TypeError, OverflowError) as e:
1555
+ raise ValueError("Input.data is not JSON serializable") from e
1556
+ elif isinstance(self.input, dict):
1557
+ try:
1558
+ _ = serialize_json(self.input)
1559
+ except (TypeError, OverflowError) as e:
1560
+ raise ValueError("Input is dict[str, Any] but it is not JSON serializable") from e
1561
+
1562
+ if isinstance(self.output, Output):
1563
+ try:
1564
+ _ = serialize_json(self.output.solution)
1565
+ except (TypeError, OverflowError) as e:
1566
+ raise ValueError("`Output.solution` is not JSON serializable") from e
1567
+ elif isinstance(self.output, dict):
1568
+ try:
1569
+ _ = serialize_json(self.output)
1570
+ except (TypeError, OverflowError) as e:
1571
+ raise ValueError("Output is dict[str, Any] but it is not JSON serializable") from e
1572
+
1573
+ def logs_text(self) -> str:
1574
+ """
1575
+ Returns the logs as a single string.
1576
+
1577
+ Each log entry is separated by a newline character.
1578
+
1579
+ Returns
1580
+ -------
1581
+ str
1582
+ The logs as a single string. If no logs are present, an empty
1583
+ string is returned.
1584
+
1585
+ Examples
1586
+ --------
1587
+ >>> from nextmv import TrackedRun, TrackedRunStatus
1588
+ >>> run = TrackedRun(
1589
+ ... status=TrackedRunStatus.SUCCEEDED,
1590
+ ... logs=["Starting optimization", "Processing data", "Optimization complete"]
1591
+ ... )
1592
+ >>> run.logs_text()
1593
+ 'Starting optimization\\nProcessing data\\nOptimization complete'
1594
+
1595
+ >>> # Single string log
1596
+ >>> run_with_string_log = TrackedRun(
1597
+ ... status=TrackedRunStatus.SUCCEEDED,
1598
+ ... logs="Single log entry"
1599
+ ... )
1600
+ >>> run_with_string_log.logs_text()
1601
+ 'Single log entry'
1602
+
1603
+ >>> # No logs
1604
+ >>> run_no_logs = TrackedRun(status=TrackedRunStatus.SUCCEEDED)
1605
+ >>> run_no_logs.logs_text()
1606
+ ''
1607
+
1608
+ Raises
1609
+ ------
1610
+ TypeError
1611
+ If `self.logs` is not a string or a list of strings.
1612
+ """
1613
+
1614
+ if self.logs is None:
1615
+ return ""
1616
+
1617
+ if isinstance(self.logs, str):
1618
+ return self.logs
1619
+
1620
+ if isinstance(self.logs, list):
1621
+ return "\\n".join(self.logs)
1622
+
1623
+ raise TypeError("Logs must be a string or a list of strings.")