nextmv 0.30.0__py3-none-any.whl → 0.32.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nextmv/run.py ADDED
@@ -0,0 +1,1460 @@
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, Optional, Union
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 Output, OutputFormat
56
+ from nextmv.status import Status, StatusV2
57
+
58
+
59
+ def run_duration(start: Union[datetime, float], end: Union[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: Optional[FormatOutput] = 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: Optional[RunType] = Field(
326
+ serialization_alias="type",
327
+ validation_alias=AliasChoices("type", "run_type"),
328
+ default=None,
329
+ )
330
+ """Type of the run."""
331
+ definition_id: Optional[str] = None
332
+ """ID of the definition for the run type."""
333
+ reference_id: Optional[str] = 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: Optional[str] = None
432
+ """Error message if the statistics could not be retrieved."""
433
+ indicators: Optional[list[StatisticsIndicator]] = 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: Optional[Status] = None
602
+ """Deprecated, use status_v2 instead."""
603
+ queuing_priority: Optional[int] = None
604
+ """Priority of the run in the queue."""
605
+ queuing_disabled: Optional[bool] = None
606
+ """Whether the run is disabled from queuing."""
607
+ experiment_id: Optional[str] = None
608
+ """ID of the experiment associated with the run."""
609
+ statistics: Optional[RunInfoStatistics] = None
610
+ """Statistics of the run."""
611
+ input_id: Optional[str] = None
612
+ """ID of the input associated with the run."""
613
+ option_set: Optional[str] = None
614
+ """ID of the option set associated with the run."""
615
+ options: Optional[dict[str, str]] = None
616
+ """Options associated with the run."""
617
+ request_options: Optional[dict[str, str]] = None
618
+ """Request options associated with the run."""
619
+ options_summary: Optional[list[OptionsSummaryItem]] = None
620
+ """Summary of options used in the run."""
621
+ scenario_id: Optional[str] = None
622
+ """ID of the scenario associated with the run."""
623
+ repetition: Optional[int] = None
624
+ """Repetition number of the run."""
625
+ input_set_id: Optional[str] = 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
+
686
+ status: Optional[Status] = None
687
+ """Deprecated: use status_v2."""
688
+
689
+
690
+ class RunInformation(BaseModel):
691
+ """
692
+ Information of a run.
693
+
694
+ You can import the `RunInformation` class directly from `nextmv`:
695
+
696
+ ```python
697
+ from nextmv import RunInformation
698
+ ```
699
+
700
+ Parameters
701
+ ----------
702
+ description : str
703
+ Description of the run.
704
+ id : str
705
+ ID of the run.
706
+ metadata : Metadata
707
+ Metadata of the run.
708
+ name : str
709
+ Name of the run.
710
+ user_email : str
711
+ Email of the user who submitted the run.
712
+ console_url : str, optional
713
+ URL to the run in the Nextmv console. Defaults to "".
714
+ """
715
+
716
+ description: str
717
+ """Description of the run."""
718
+ id: str
719
+ """ID of the run."""
720
+ metadata: Metadata
721
+ """Metadata of the run."""
722
+ name: str
723
+ """Name of the run."""
724
+ user_email: str
725
+ """Email of the user who submitted the run."""
726
+ console_url: str = Field(default="")
727
+ """
728
+ URL to the run in the Nextmv console.
729
+ """
730
+ synced_run_id: Optional[str] = None
731
+ """
732
+ ID of the synced remote run, if applicable. When the `Application.sync`
733
+ method is used, this field marks the association between the local run
734
+ (`id`) and the remote run (`synced_run_id`). This field is None if the run
735
+ was not created using `Application.sync` or if the run has not been synced
736
+ yet.
737
+ """
738
+ synced_at: Optional[datetime] = None
739
+ """
740
+ Timestamp when the run was synced with the remote run. This field is
741
+ None if the run was not created using `Application.sync` or if the run
742
+ has not been synced yet.
743
+ """
744
+
745
+ def to_run(self) -> Run:
746
+ """
747
+ Transform this `RunInformation` instance into a `Run` instance.
748
+
749
+ This method maps all available attributes from the `RunInformation`
750
+ and its metadata to create a `Run` instance. Attributes that are not
751
+ available in RunInformation are set to None or appropriate defaults.
752
+
753
+ Returns
754
+ -------
755
+ Run
756
+ A Run instance with attributes populated from this RunInformation.
757
+
758
+ Examples
759
+ --------
760
+ >>> from nextmv import RunInformation, Metadata, Format, FormatInput, FormatOutput
761
+ >>> from nextmv import StatusV2, RunTypeConfiguration, RunType
762
+ >>> from datetime import datetime
763
+ >>> metadata = Metadata(
764
+ ... application_id="app-123",
765
+ ... application_instance_id="instance-456",
766
+ ... application_version_id="version-789",
767
+ ... created_at=datetime.now(),
768
+ ... duration=5000.0,
769
+ ... error="",
770
+ ... input_size=1024.0,
771
+ ... output_size=2048.0,
772
+ ... format=Format(
773
+ ... format_input=FormatInput(),
774
+ ... format_output=FormatOutput()
775
+ ... ),
776
+ ... status_v2=StatusV2.SUCCEEDED
777
+ ... )
778
+ >>> run_info = RunInformation(
779
+ ... id="run-123",
780
+ ... description="Test run",
781
+ ... name="Test",
782
+ ... user_email="user@example.com",
783
+ ... metadata=metadata
784
+ ... )
785
+ >>> run = run_info.to_run()
786
+ >>> run.id
787
+ 'run-123'
788
+ >>> run.application_id
789
+ 'app-123'
790
+ """
791
+ return Run(
792
+ id=self.id,
793
+ user_email=self.user_email,
794
+ name=self.name,
795
+ description=self.description,
796
+ created_at=self.metadata.created_at,
797
+ application_id=self.metadata.application_id,
798
+ application_instance_id=self.metadata.application_instance_id,
799
+ application_version_id=self.metadata.application_version_id,
800
+ run_type=RunTypeConfiguration(), # Default empty configuration
801
+ execution_class="", # Not available in RunInformation
802
+ runtime="", # Not available in RunInformation
803
+ status=self.metadata.status,
804
+ status_v2=self.metadata.status_v2,
805
+ # Optional fields that are not available in RunInformation
806
+ queuing_priority=None,
807
+ queuing_disabled=None,
808
+ experiment_id=None,
809
+ statistics=None,
810
+ input_id=None,
811
+ option_set=None,
812
+ options=None,
813
+ request_options=None,
814
+ options_summary=None,
815
+ scenario_id=None,
816
+ repetition=None,
817
+ input_set_id=None,
818
+ )
819
+
820
+
821
+ class ErrorLog(BaseModel):
822
+ """
823
+ Error log of a run, when it was not successful.
824
+
825
+ You can import the `ErrorLog` class directly from `nextmv`:
826
+
827
+ ```python
828
+ from nextmv import ErrorLog
829
+ ```
830
+
831
+ Parameters
832
+ ----------
833
+ error : str, optional
834
+ Error message. Defaults to None.
835
+ stdout : str, optional
836
+ Standard output. Defaults to None.
837
+ stderr : str, optional
838
+ Standard error. Defaults to None.
839
+ """
840
+
841
+ error: Optional[str] = None
842
+ """Error message."""
843
+ stdout: Optional[str] = None
844
+ """Standard output."""
845
+ stderr: Optional[str] = None
846
+ """Standard error."""
847
+
848
+
849
+ class RunResult(RunInformation):
850
+ """
851
+ Result of a run, whether it was successful or not.
852
+
853
+ You can import the `RunResult` class directly from `nextmv`:
854
+
855
+ ```python
856
+ from nextmv import RunResult
857
+ ```
858
+
859
+ Parameters
860
+ ----------
861
+ error_log : ErrorLog, optional
862
+ Error log of the run. Only available if the run failed. Defaults to
863
+ None.
864
+ output : dict[str, Any], optional
865
+ Output of the run. Only available if the run succeeded. Defaults to
866
+ None.
867
+ """
868
+
869
+ error_log: Optional[ErrorLog] = None
870
+ """Error log of the run. Only available if the run failed."""
871
+ output: Optional[dict[str, Any]] = None
872
+ """Output of the run. Only available if the run succeeded."""
873
+
874
+
875
+ class RunLog(BaseModel):
876
+ """
877
+ Log of a run.
878
+
879
+ You can import the `RunLog` class directly from `nextmv`:
880
+
881
+ ```python
882
+ from nextmv import RunLog
883
+ ```
884
+
885
+ Parameters
886
+ ----------
887
+ log : str
888
+ Log of the run.
889
+
890
+ Examples
891
+ --------
892
+ >>> from nextmv import RunLog
893
+ >>> run_log = RunLog(log="Optimization completed successfully")
894
+ >>> run_log.log
895
+ 'Optimization completed successfully'
896
+
897
+ >>> # Multi-line log
898
+ >>> multi_line_log = RunLog(log="Starting optimization\\nProcessing data\\nCompleted")
899
+ >>> multi_line_log.log
900
+ 'Starting optimization\\nProcessing data\\nCompleted'
901
+ """
902
+
903
+ log: str
904
+ """Log of the run."""
905
+
906
+
907
+ class RunQueuing(BaseModel):
908
+ """
909
+ RunQueuing configuration for a run.
910
+
911
+ You can import the `RunQueuing` class directly from `nextmv`:
912
+
913
+ ```python
914
+ from nextmv import RunQueuing
915
+ ```
916
+
917
+ Parameters
918
+ ----------
919
+ priority : int, optional
920
+ Priority of the run in the queue. 1 is the highest priority, 9 is the
921
+ lowest priority. Defaults to None.
922
+ disabled : bool, optional
923
+ Whether the run should be queued, or not. If True, the run will not be
924
+ queued. If False, the run will be queued. Defaults to None.
925
+
926
+ Examples
927
+ --------
928
+ >>> from nextmv import RunQueuing
929
+ >>> queuing = RunQueuing(priority=1, disabled=False)
930
+ >>> queuing.priority
931
+ 1
932
+ >>> queuing.disabled
933
+ False
934
+
935
+ >>> # High priority run
936
+ >>> high_priority = RunQueuing(priority=1)
937
+ >>> high_priority.priority
938
+ 1
939
+
940
+ >>> # Disabled queuing
941
+ >>> no_queue = RunQueuing(disabled=True)
942
+ >>> no_queue.disabled
943
+ True
944
+ """
945
+
946
+ priority: Optional[int] = None
947
+ """
948
+ Priority of the run in the queue. 1 is the highest priority, 9 is the
949
+ lowest priority.
950
+ """
951
+ disabled: Optional[bool] = None
952
+ """
953
+ Whether the run should be queued, or not. If True, the run will not be
954
+ queued. If False, the run will be queued.
955
+ """
956
+
957
+ def __post_init_post_parse__(self):
958
+ """
959
+ Validations done after parsing the model.
960
+
961
+ Raises
962
+ ------
963
+ ValueError
964
+ If priority is not between 1 and 9, or if disabled is not a
965
+ boolean value.
966
+ """
967
+
968
+ if self.priority is not None and (self.priority < 1 or self.priority > 9):
969
+ raise ValueError("Priority must be between 1 and 9.")
970
+
971
+ if self.disabled is not None and self.disabled not in {True, False}:
972
+ raise ValueError("Disabled must be a boolean value.")
973
+
974
+
975
+ class RunConfiguration(BaseModel):
976
+ """
977
+ Configuration for an app run.
978
+
979
+ You can import the `RunConfiguration` class directly from `nextmv`:
980
+
981
+ ```python
982
+ from nextmv import RunConfiguration
983
+ ```
984
+
985
+ Parameters
986
+ ----------
987
+ execution_class : str, optional
988
+ Execution class for the instance. Defaults to None.
989
+ format : Format, optional
990
+ Format for the run configuration. Defaults to None.
991
+ run_type : RunTypeConfiguration, optional
992
+ Run type configuration for the run. Defaults to None.
993
+ secrets_collection_id : str, optional
994
+ ID of the secrets collection to use for the run. Defaults to None.
995
+ queuing : RunQueuing, optional
996
+ Queuing configuration for the run. Defaults to None.
997
+
998
+ Examples
999
+ --------
1000
+ >>> from nextmv import RunConfiguration, RunQueuing
1001
+ >>> config = RunConfiguration(
1002
+ ... execution_class="large",
1003
+ ... queuing=RunQueuing(priority=1)
1004
+ ... )
1005
+ >>> config.execution_class
1006
+ 'large'
1007
+ >>> config.queuing.priority
1008
+ 1
1009
+
1010
+ >>> # Basic configuration
1011
+ >>> basic_config = RunConfiguration()
1012
+ >>> basic_config.format is None
1013
+ True
1014
+ """
1015
+
1016
+ execution_class: Optional[str] = None
1017
+ """Execution class for the instance."""
1018
+ format: Optional[Format] = None
1019
+ """Format for the run configuration."""
1020
+ run_type: Optional[RunTypeConfiguration] = None
1021
+ """Run type configuration for the run."""
1022
+ secrets_collection_id: Optional[str] = None
1023
+ """ID of the secrets collection to use for the run."""
1024
+ queuing: Optional[RunQueuing] = None
1025
+ """Queuing configuration for the run."""
1026
+
1027
+ def resolve(
1028
+ self,
1029
+ input: Union[Input, dict[str, Any], BaseModel, str],
1030
+ dir_path: Optional[str] = None,
1031
+ ) -> None:
1032
+ """
1033
+ Resolves the run configuration by modifying or setting the `format`,
1034
+ based on the type of input that is provided.
1035
+
1036
+ Parameters
1037
+ ----------
1038
+ input : Input or dict[str, Any] or BaseModel or str
1039
+ The input to use for resolving the run configuration.
1040
+ dir_path : str, optional
1041
+ The directory path where inputs can be loaded from.
1042
+
1043
+ Examples
1044
+ --------
1045
+ >>> from nextmv import RunConfiguration
1046
+ >>> config = RunConfiguration()
1047
+ >>> config.resolve({"key": "value"})
1048
+ >>> config.format.format_input.input_type
1049
+ <InputFormat.JSON: 'json'>
1050
+
1051
+ >>> config = RunConfiguration()
1052
+ >>> config.resolve("text input")
1053
+ >>> config.format.format_input.input_type
1054
+ <InputFormat.TEXT: 'text'>
1055
+
1056
+ >>> config = RunConfiguration()
1057
+ >>> config.resolve({}, dir_path="/path/to/files")
1058
+ >>> config.format.format_input.input_type
1059
+ <InputFormat.MULTI_FILE: 'multi_file'>
1060
+ """
1061
+
1062
+ # If the value is set by the user, do not change it.
1063
+ if self.format is not None:
1064
+ return
1065
+
1066
+ self.format = Format(
1067
+ format_input=FormatInput(input_type=InputFormat.JSON),
1068
+ format_output=FormatOutput(output_type=OutputFormat.JSON),
1069
+ )
1070
+
1071
+ if isinstance(input, dict):
1072
+ self.format.format_input.input_type = InputFormat.JSON
1073
+ elif isinstance(input, str):
1074
+ self.format.format_input.input_type = InputFormat.TEXT
1075
+ elif dir_path is not None and dir_path != "":
1076
+ # Kinda hard to detect if we should be working with CSV_ARCHIVE or
1077
+ # MULTI_FILE, so we default to MULTI_FILE.
1078
+ self.format.format_input.input_type = InputFormat.MULTI_FILE
1079
+ elif isinstance(input, Input):
1080
+ self.format.format_input.input_type = input.input_format
1081
+
1082
+ # As input and output are symmetric, we set the output according to the input
1083
+ # format.
1084
+ if self.format.format_input.input_type == InputFormat.JSON:
1085
+ self.format.format_output = FormatOutput(output_type=OutputFormat.JSON)
1086
+ elif self.format.format_input.input_type == InputFormat.TEXT: # Text still maps to json
1087
+ self.format.format_output = FormatOutput(output_type=OutputFormat.JSON)
1088
+ elif self.format.format_input.input_type == InputFormat.CSV_ARCHIVE:
1089
+ self.format.format_output = FormatOutput(output_type=OutputFormat.CSV_ARCHIVE)
1090
+ elif self.format.format_input.input_type == InputFormat.MULTI_FILE:
1091
+ self.format.format_output = FormatOutput(output_type=OutputFormat.MULTI_FILE)
1092
+ else:
1093
+ self.format.format_output = FormatOutput(output_type=OutputFormat.JSON)
1094
+
1095
+
1096
+ class ExternalRunResult(BaseModel):
1097
+ """
1098
+ Result of a run used to configure a new application run as an
1099
+ external one.
1100
+
1101
+ You can import the `ExternalRunResult` class directly from `nextmv`:
1102
+
1103
+ ```python
1104
+ from nextmv import ExternalRunResult
1105
+ ```
1106
+
1107
+ Parameters
1108
+ ----------
1109
+ output_upload_id : str, optional
1110
+ ID of the output upload. Defaults to None.
1111
+ error_upload_id : str, optional
1112
+ ID of the error upload. Defaults to None.
1113
+ status : str, optional
1114
+ Status of the run. Must be "succeeded" or "failed". Defaults to None.
1115
+ error_message : str, optional
1116
+ Error message of the run. Defaults to None.
1117
+ execution_duration : int, optional
1118
+ Duration of the run, in milliseconds. Defaults to None.
1119
+
1120
+ Examples
1121
+ --------
1122
+ >>> from nextmv import ExternalRunResult
1123
+ >>> # Successful external run
1124
+ >>> result = ExternalRunResult(
1125
+ ... output_upload_id="upload-12345",
1126
+ ... status="succeeded",
1127
+ ... execution_duration=5000
1128
+ ... )
1129
+ >>> result.status
1130
+ 'succeeded'
1131
+ >>> result.execution_duration
1132
+ 5000
1133
+
1134
+ >>> # Failed external run
1135
+ >>> failed_result = ExternalRunResult(
1136
+ ... error_upload_id="error-67890",
1137
+ ... status="failed",
1138
+ ... error_message="Optimization failed due to invalid constraints",
1139
+ ... execution_duration=2000
1140
+ ... )
1141
+ >>> failed_result.status
1142
+ 'failed'
1143
+ >>> failed_result.error_message
1144
+ 'Optimization failed due to invalid constraints'
1145
+ """
1146
+
1147
+ output_upload_id: Optional[str] = None
1148
+ """ID of the output upload."""
1149
+ error_upload_id: Optional[str] = None
1150
+ """ID of the error upload."""
1151
+ status: Optional[str] = None
1152
+ """Status of the run."""
1153
+ error_message: Optional[str] = None
1154
+ """Error message of the run."""
1155
+ execution_duration: Optional[int] = None
1156
+ """Duration of the run, in milliseconds."""
1157
+
1158
+ def __post_init_post_parse__(self):
1159
+ """
1160
+ Validations done after parsing the model.
1161
+
1162
+ Raises
1163
+ ------
1164
+ ValueError
1165
+ If the status value is not "succeeded" or "failed".
1166
+ """
1167
+
1168
+ valid_statuses = {"succeeded", "failed"}
1169
+ if self.status is not None and self.status not in valid_statuses:
1170
+ raise ValueError("Invalid status value, must be one of: " + ", ".join(valid_statuses))
1171
+
1172
+
1173
+ class TrackedRunStatus(str, Enum):
1174
+ """
1175
+ The status of a tracked run.
1176
+
1177
+ You can import the `TrackedRunStatus` class directly from `nextmv`:
1178
+
1179
+ ```python
1180
+ from nextmv import TrackedRunStatus
1181
+ ```
1182
+
1183
+ Parameters
1184
+ ----------
1185
+ SUCCEEDED : str
1186
+ The run succeeded.
1187
+ FAILED : str
1188
+ The run failed.
1189
+
1190
+ Examples
1191
+ --------
1192
+ >>> from nextmv import TrackedRunStatus
1193
+ >>> status = TrackedRunStatus.SUCCEEDED
1194
+ >>> status
1195
+ <TrackedRunStatus.SUCCEEDED: 'succeeded'>
1196
+ >>> status.value
1197
+ 'succeeded'
1198
+
1199
+ >>> # Creating from string
1200
+ >>> failed_status = TrackedRunStatus("failed")
1201
+ >>> failed_status
1202
+ <TrackedRunStatus.FAILED: 'failed'>
1203
+
1204
+ >>> # All available statuses
1205
+ >>> list(TrackedRunStatus)
1206
+ [<TrackedRunStatus.SUCCEEDED: 'succeeded'>, <TrackedRunStatus.FAILED: 'failed'>]
1207
+ """
1208
+
1209
+ SUCCEEDED = "succeeded"
1210
+ """The run succeeded."""
1211
+ FAILED = "failed"
1212
+ """The run failed."""
1213
+
1214
+
1215
+ @dataclass
1216
+ class TrackedRun:
1217
+ """
1218
+ An external run that is tracked in the Nextmv platform.
1219
+
1220
+ You can import the `TrackedRun` class directly from `nextmv`:
1221
+
1222
+ ```python
1223
+ from nextmv import TrackedRun
1224
+ ```
1225
+
1226
+ Parameters
1227
+ ----------
1228
+ status : TrackedRunStatus
1229
+ The status of the run being tracked. This field is required.
1230
+ input : Input or dict[str, Any] or str, optional
1231
+ The input of the run being tracked. Please note that if the input
1232
+ format is JSON, then the input data must be JSON serializable. If both
1233
+ `input` and `input_dir_path` are specified, the `input` is ignored, and
1234
+ the files in the directory are used instead. Defaults to None.
1235
+ output : Output or dict[str, Any] or str, optional
1236
+ The output of the run being tracked. Please note that if the output
1237
+ format is JSON, then the output data must be JSON serializable. If both
1238
+ `output` and `output_dir_path` are specified, the `output` is ignored, and
1239
+ the files in the directory are used instead. Defaults to None.
1240
+ duration : int, optional
1241
+ The duration of the run being tracked, in milliseconds. This field is
1242
+ optional. Defaults to None.
1243
+ error : str, optional
1244
+ An error message if the run failed. You should only specify this if the
1245
+ run failed (the `status` is `TrackedRunStatus.FAILED`), otherwise an
1246
+ exception will be raised. This field is optional. Defaults to None.
1247
+ logs : list[str], optional
1248
+ The logs of the run being tracked. Each element of the list is a line in
1249
+ the log. This field is optional. Defaults to None.
1250
+ name : str, optional
1251
+ Optional name for the run being tracked. Defaults to None.
1252
+ description : str, optional
1253
+ Optional description for the run being tracked. Defaults to None.
1254
+ input_dir_path : str, optional
1255
+ Path to a directory containing input files. If specified, the calling
1256
+ function will package the files in the directory into a tar file and upload
1257
+ it as a large input. This is useful for non-JSON input formats, such as
1258
+ when working with `CSV_ARCHIVE` or `MULTI_FILE`. If both `input` and
1259
+ `input_dir_path` are specified, the `input` is ignored, and the files in
1260
+ the directory are used instead. Defaults to None.
1261
+ output_dir_path : str, optional
1262
+ Path to a directory containing output files. If specified, the calling
1263
+ function will package the files in the directory into a tar file and upload
1264
+ it as a large output. This is useful for non-JSON output formats, such as
1265
+ when working with `CSV_ARCHIVE` or `MULTI_FILE`. If both `output` and
1266
+ `output_dir_path` are specified, the `output` is ignored, and the files
1267
+ are saved in the directory instead. Defaults to None.
1268
+
1269
+ Examples
1270
+ --------
1271
+ >>> from nextmv import TrackedRun, TrackedRunStatus
1272
+ >>> # Successful run
1273
+ >>> run = TrackedRun(
1274
+ ... status=TrackedRunStatus.SUCCEEDED,
1275
+ ... input={"vehicles": 5, "locations": 10},
1276
+ ... output={"routes": [{"stops": [1, 2, 3]}]},
1277
+ ... duration=5000,
1278
+ ... name="test-run",
1279
+ ... description="A test optimization run"
1280
+ ... )
1281
+ >>> run.status
1282
+ <TrackedRunStatus.SUCCEEDED: 'succeeded'>
1283
+ >>> run.duration
1284
+ 5000
1285
+
1286
+ >>> # Failed run with error
1287
+ >>> failed_run = TrackedRun(
1288
+ ... status=TrackedRunStatus.FAILED,
1289
+ ... input={"vehicles": 0},
1290
+ ... error="No vehicles available for routing",
1291
+ ... duration=1000,
1292
+ ... logs=["Starting optimization", "Error: No vehicles found"]
1293
+ ... )
1294
+ >>> failed_run.status
1295
+ <TrackedRunStatus.FAILED: 'failed'>
1296
+ >>> failed_run.error
1297
+ 'No vehicles available for routing'
1298
+
1299
+ >>> # Run with directory-based input/output
1300
+ >>> dir_run = TrackedRun(
1301
+ ... status=TrackedRunStatus.SUCCEEDED,
1302
+ ... input_dir_path="/path/to/input/files",
1303
+ ... output_dir_path="/path/to/output/files",
1304
+ ... duration=10000
1305
+ ... )
1306
+ >>> dir_run.input_dir_path
1307
+ '/path/to/input/files'
1308
+
1309
+ Raises
1310
+ ------
1311
+ ValueError
1312
+ If the status value is invalid, if an error message is provided for a
1313
+ successful run, or if input/output formats are not JSON or
1314
+ input/output dicts are not JSON serializable.
1315
+ """
1316
+
1317
+ status: TrackedRunStatus
1318
+ """The status of the run being tracked"""
1319
+
1320
+ input: Optional[Union[Input, dict[str, Any], str]] = None
1321
+ """
1322
+ The input of the run being tracked. Please note that if the input
1323
+ format is JSON, then the input data must be JSON serializable. If both
1324
+ `input` and `input_dir_path` are specified, the `input` is ignored, and
1325
+ the files in the directory are used instead.
1326
+ """
1327
+ output: Optional[Union[Output, dict[str, Any], str]] = None
1328
+ """
1329
+ The output of the run being tracked. Please note that if the output
1330
+ format is JSON, then the output data must be JSON serializable. If both
1331
+ `output` and `output_dir_path` are specified, the `output` is ignored, and
1332
+ the files in the directory are used instead.
1333
+ """
1334
+ duration: Optional[int] = None
1335
+ """The duration of the run being tracked, in milliseconds."""
1336
+ error: Optional[str] = None
1337
+ """An error message if the run failed. You should only specify this if the
1338
+ run failed, otherwise an exception will be raised."""
1339
+ logs: Optional[list[str]] = None
1340
+ """The logs of the run being tracked. Each element of the list is a line in
1341
+ the log."""
1342
+ name: Optional[str] = None
1343
+ """
1344
+ Optional name for the run being tracked.
1345
+ """
1346
+ description: Optional[str] = None
1347
+ """
1348
+ Optional description for the run being tracked.
1349
+ """
1350
+ input_dir_path: Optional[str] = None
1351
+ """
1352
+ Path to a directory containing input files. If specified, the calling
1353
+ function will package the files in the directory into a tar file and upload
1354
+ it as a large input. This is useful for non-JSON input formats, such as
1355
+ when working with `CSV_ARCHIVE` or `MULTI_FILE`. If both `input` and
1356
+ `input_dir_path` are specified, the `input` is ignored, and the files in
1357
+ the directory are used instead.
1358
+ """
1359
+ output_dir_path: Optional[str] = None
1360
+ """
1361
+ Path to a directory containing output files. If specified, the calling
1362
+ function will package the files in the directory into a tar file and upload
1363
+ it as a large output. This is useful for non-JSON output formats, such as
1364
+ when working with `CSV_ARCHIVE` or `MULTI_FILE`. If both `output` and
1365
+ `output_dir_path` are specified, the `output` is ignored, and the files
1366
+ are saved in the directory instead.
1367
+ """
1368
+
1369
+ def __post_init__(self): # noqa: C901
1370
+ """
1371
+ Validations done after parsing the model.
1372
+
1373
+ Raises
1374
+ ------
1375
+ ValueError
1376
+ If the status value is invalid, if an error message is provided for
1377
+ a successful run, or if input/output formats are not JSON or
1378
+ input/output dicts are not JSON serializable.
1379
+ """
1380
+
1381
+ valid_statuses = {TrackedRunStatus.SUCCEEDED, TrackedRunStatus.FAILED}
1382
+ if self.status not in valid_statuses:
1383
+ raise ValueError("Invalid status value, must be one of: " + ", ".join(valid_statuses))
1384
+
1385
+ if self.error is not None and self.error != "" and self.status != TrackedRunStatus.FAILED:
1386
+ raise ValueError("Error message must be empty if the run succeeded.")
1387
+
1388
+ if isinstance(self.input, Input):
1389
+ try:
1390
+ _ = serialize_json(self.input.data)
1391
+ except (TypeError, OverflowError) as e:
1392
+ raise ValueError("Input.data is not JSON serializable") from e
1393
+ elif isinstance(self.input, dict):
1394
+ try:
1395
+ _ = serialize_json(self.input)
1396
+ except (TypeError, OverflowError) as e:
1397
+ raise ValueError("Input is dict[str, Any] but it is not JSON serializable") from e
1398
+
1399
+ if isinstance(self.output, Output):
1400
+ try:
1401
+ _ = serialize_json(self.output.data)
1402
+ except (TypeError, OverflowError) as e:
1403
+ raise ValueError("Output.data is not JSON serializable") from e
1404
+ elif isinstance(self.output, dict):
1405
+ try:
1406
+ _ = serialize_json(self.output)
1407
+ except (TypeError, OverflowError) as e:
1408
+ raise ValueError("Output is dict[str, Any] but it is not JSON serializable") from e
1409
+
1410
+ def logs_text(self) -> str:
1411
+ """
1412
+ Returns the logs as a single string.
1413
+
1414
+ Each log entry is separated by a newline character.
1415
+
1416
+ Returns
1417
+ -------
1418
+ str
1419
+ The logs as a single string. If no logs are present, an empty
1420
+ string is returned.
1421
+
1422
+ Examples
1423
+ --------
1424
+ >>> from nextmv import TrackedRun, TrackedRunStatus
1425
+ >>> run = TrackedRun(
1426
+ ... status=TrackedRunStatus.SUCCEEDED,
1427
+ ... logs=["Starting optimization", "Processing data", "Optimization complete"]
1428
+ ... )
1429
+ >>> run.logs_text()
1430
+ 'Starting optimization\\nProcessing data\\nOptimization complete'
1431
+
1432
+ >>> # Single string log
1433
+ >>> run_with_string_log = TrackedRun(
1434
+ ... status=TrackedRunStatus.SUCCEEDED,
1435
+ ... logs="Single log entry"
1436
+ ... )
1437
+ >>> run_with_string_log.logs_text()
1438
+ 'Single log entry'
1439
+
1440
+ >>> # No logs
1441
+ >>> run_no_logs = TrackedRun(status=TrackedRunStatus.SUCCEEDED)
1442
+ >>> run_no_logs.logs_text()
1443
+ ''
1444
+
1445
+ Raises
1446
+ ------
1447
+ TypeError
1448
+ If `self.logs` is not a string or a list of strings.
1449
+ """
1450
+
1451
+ if self.logs is None:
1452
+ return ""
1453
+
1454
+ if isinstance(self.logs, str):
1455
+ return self.logs
1456
+
1457
+ if isinstance(self.logs, list):
1458
+ return "\\n".join(self.logs)
1459
+
1460
+ raise TypeError("Logs must be a string or a list of strings.")