nextmv 0.26.3__py3-none-any.whl → 0.28.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/output.py CHANGED
@@ -1,4 +1,44 @@
1
- """Module for handling output destinations and data."""
1
+ """
2
+ Module for handling output destinations and data.
3
+
4
+ This module provides classes and functions for handling the output of decision
5
+ problems, including formatting, serialization, and writing to various
6
+ destinations.
7
+
8
+ Classes
9
+ -------
10
+ RunStatistics
11
+ Statistics about a general run.
12
+ ResultStatistics
13
+ Statistics about a specific result.
14
+ DataPoint
15
+ A data point representing a 2D coordinate.
16
+ Series
17
+ A series of data points for visualization or analysis.
18
+ SeriesData
19
+ Data container for multiple series of data points.
20
+ Statistics
21
+ Complete statistics container for a solution, including run metrics and result data.
22
+ OutputFormat
23
+ Enumeration of supported output formats.
24
+ VisualSchema
25
+ Enumeration of supported visualization schemas.
26
+ Visual
27
+ Visual schema definition for an asset.
28
+ Asset
29
+ Represents downloadable information that is part of the `Output`.
30
+ Output
31
+ A class for representing the output of a decision problem.
32
+ OutputWriter
33
+ Base class for writing outputs to different destinations.
34
+ LocalOutputWriter
35
+ Class for writing outputs to local files or stdout.
36
+
37
+ Functions
38
+ ---------
39
+ write
40
+ Write the output to the specified destination.
41
+ """
2
42
 
3
43
  import copy
4
44
  import csv
@@ -22,6 +62,12 @@ class RunStatistics(BaseModel):
22
62
  """
23
63
  Statistics about a general run.
24
64
 
65
+ You can import the `RunStatistics` class directly from `nextmv`:
66
+
67
+ ```python
68
+ from nextmv import RunStatistics
69
+ ```
70
+
25
71
  Parameters
26
72
  ----------
27
73
  duration : float, optional
@@ -31,6 +77,16 @@ class RunStatistics(BaseModel):
31
77
  custom : Union[Any, dict[str, Any]], optional
32
78
  Custom statistics created by the user. Can normally expect a `dict[str,
33
79
  Any]`.
80
+
81
+ Examples
82
+ --------
83
+ >>> from nextmv.output import RunStatistics
84
+ >>> stats = RunStatistics(duration=10.5, iterations=100)
85
+ >>> stats.duration
86
+ 10.5
87
+ >>> stats.custom = {"convergence": 0.001}
88
+ >>> stats.to_dict()
89
+ {'duration': 10.5, 'iterations': 100, 'custom': {'convergence': 0.001}}
34
90
  """
35
91
 
36
92
  duration: Optional[float] = None
@@ -51,6 +107,12 @@ class ResultStatistics(BaseModel):
51
107
  """
52
108
  Statistics about a specific result.
53
109
 
110
+ You can import the `ResultStatistics` class directly from `nextmv`:
111
+
112
+ ```python
113
+ from nextmv import ResultStatistics
114
+ ```
115
+
54
116
  Parameters
55
117
  ----------
56
118
  duration : float, optional
@@ -60,6 +122,16 @@ class ResultStatistics(BaseModel):
60
122
  custom : Union[Any, dict[str, Any]], optional
61
123
  Custom statistics created by the user. Can normally expect a `dict[str,
62
124
  Any]`.
125
+
126
+ Examples
127
+ --------
128
+ >>> from nextmv.output import ResultStatistics
129
+ >>> result_stats = ResultStatistics(duration=5.2, value=42.0)
130
+ >>> result_stats.value
131
+ 42.0
132
+ >>> result_stats.custom = {"gap": 0.05}
133
+ >>> result_stats.to_dict()
134
+ {'duration': 5.2, 'value': 42.0, 'custom': {'gap': 0.05}}
63
135
  """
64
136
 
65
137
  duration: Optional[float] = None
@@ -78,7 +150,13 @@ class ResultStatistics(BaseModel):
78
150
 
79
151
  class DataPoint(BaseModel):
80
152
  """
81
- A data point.
153
+ A data point representing a 2D coordinate.
154
+
155
+ You can import the `DataPoint` class directly from `nextmv`:
156
+
157
+ ```python
158
+ from nextmv import DataPoint
159
+ ```
82
160
 
83
161
  Parameters
84
162
  ----------
@@ -86,6 +164,15 @@ class DataPoint(BaseModel):
86
164
  X coordinate of the data point.
87
165
  y : float
88
166
  Y coordinate of the data point.
167
+
168
+ Examples
169
+ --------
170
+ >>> from nextmv.output import DataPoint
171
+ >>> point = DataPoint(x=3.5, y=4.2)
172
+ >>> point.x
173
+ 3.5
174
+ >>> point.to_dict()
175
+ {'x': 3.5, 'y': 4.2}
89
176
  """
90
177
 
91
178
  x: float
@@ -96,14 +183,30 @@ class DataPoint(BaseModel):
96
183
 
97
184
  class Series(BaseModel):
98
185
  """
99
- A series of data points.
186
+ A series of data points for visualization or analysis.
187
+
188
+ You can import the `Series` class directly from `nextmv`:
189
+
190
+ ```python
191
+ from nextmv import Series
192
+ ```
100
193
 
101
194
  Parameters
102
195
  ----------
103
196
  name : str, optional
104
197
  Name of the series.
105
198
  data_points : list[DataPoint], optional
106
- Data of the series.
199
+ Data points of the series.
200
+
201
+ Examples
202
+ --------
203
+ >>> from nextmv.output import Series, DataPoint
204
+ >>> points = [DataPoint(x=1.0, y=2.0), DataPoint(x=2.0, y=3.0)]
205
+ >>> series = Series(name="Example Series", data_points=points)
206
+ >>> series.name
207
+ 'Example Series'
208
+ >>> len(series.data_points)
209
+ 2
107
210
  """
108
211
 
109
212
  name: Optional[str] = None
@@ -114,7 +217,13 @@ class Series(BaseModel):
114
217
 
115
218
  class SeriesData(BaseModel):
116
219
  """
117
- Data of a series.
220
+ Data container for multiple series of data points.
221
+
222
+ You can import the `SeriesData` class directly from `nextmv`:
223
+
224
+ ```python
225
+ from nextmv import SeriesData
226
+ ```
118
227
 
119
228
  Parameters
120
229
  ----------
@@ -122,6 +231,17 @@ class SeriesData(BaseModel):
122
231
  A series for the value of the solution.
123
232
  custom : list[Series], optional
124
233
  A list of series for custom statistics.
234
+
235
+ Examples
236
+ --------
237
+ >>> from nextmv.output import SeriesData, Series, DataPoint
238
+ >>> value_series = Series(name="Solution Value", data_points=[DataPoint(x=0, y=10), DataPoint(x=1, y=5)])
239
+ >>> custom_series = [Series(name="Gap", data_points=[DataPoint(x=0, y=0.5), DataPoint(x=1, y=0.1)])]
240
+ >>> series_data = SeriesData(value=value_series, custom=custom_series)
241
+ >>> series_data.value.name
242
+ 'Solution Value'
243
+ >>> len(series_data.custom)
244
+ 1
125
245
  """
126
246
 
127
247
  value: Optional[Series] = None
@@ -132,7 +252,14 @@ class SeriesData(BaseModel):
132
252
 
133
253
  class Statistics(BaseModel):
134
254
  """
135
- Statistics of a solution.
255
+ Complete statistics container for a solution, including run metrics and
256
+ result data.
257
+
258
+ You can import the `Statistics` class directly from `nextmv`:
259
+
260
+ ```python
261
+ from nextmv import Statistics
262
+ ```
136
263
 
137
264
  Parameters
138
265
  ----------
@@ -144,6 +271,17 @@ class Statistics(BaseModel):
144
271
  Series data about some metric.
145
272
  statistics_schema : str, optional
146
273
  Schema (version). This class only supports `v1`.
274
+
275
+ Examples
276
+ --------
277
+ >>> from nextmv.output import Statistics, RunStatistics, ResultStatistics
278
+ >>> run_stats = RunStatistics(duration=10.0, iterations=50)
279
+ >>> result_stats = ResultStatistics(value=100.0)
280
+ >>> stats = Statistics(run=run_stats, result=result_stats, statistics_schema="v1")
281
+ >>> stats.run.duration
282
+ 10.0
283
+ >>> stats.result.value
284
+ 100.0
147
285
  """
148
286
 
149
287
  run: Optional[RunStatistics] = None
@@ -161,7 +299,25 @@ class Statistics(BaseModel):
161
299
 
162
300
 
163
301
  class OutputFormat(str, Enum):
164
- """Format of an `Input`."""
302
+ """
303
+ Enumeration of supported output formats.
304
+
305
+ You can import the `OutputFormat` class directly from `nextmv`:
306
+
307
+ ```python
308
+ from nextmv import OutputFormat
309
+ ```
310
+
311
+ This enum defines the different formats that can be used for outputting data.
312
+ Each format has specific requirements and behaviors when writing.
313
+
314
+ Attributes
315
+ ----------
316
+ JSON : str
317
+ JSON format, utf-8 encoded.
318
+ CSV_ARCHIVE : str
319
+ CSV archive format: multiple CSV files.
320
+ """
165
321
 
166
322
  JSON = "json"
167
323
  """JSON format, utf-8 encoded."""
@@ -170,7 +326,27 @@ class OutputFormat(str, Enum):
170
326
 
171
327
 
172
328
  class VisualSchema(str, Enum):
173
- """Schema of a visual asset."""
329
+ """
330
+ Enumeration of supported visualization schemas.
331
+
332
+ You can import the `VisualSchema` class directly from `nextmv`:
333
+
334
+ ```python
335
+ from nextmv import VisualSchema
336
+ ```
337
+
338
+ This enum defines the different visualization libraries or rendering methods
339
+ that can be used to display custom asset data in the Nextmv Console.
340
+
341
+ Attributes
342
+ ----------
343
+ CHARTJS : str
344
+ Tells Nextmv Console to render the custom asset data with the Chart.js library.
345
+ GEOJSON : str
346
+ Tells Nextmv Console to render the custom asset data as GeoJSON on a map.
347
+ PLOTLY : str
348
+ Tells Nextmv Console to render the custom asset data with the Plotly library.
349
+ """
174
350
 
175
351
  CHARTJS = "chartjs"
176
352
  """Tells Nextmv Console to render the custom asset data with the Chart.js
@@ -185,8 +361,39 @@ class VisualSchema(str, Enum):
185
361
 
186
362
  class Visual(BaseModel):
187
363
  """
188
- Visual schema of an asset that defines how it is plotted in the Nextmv
189
- Console.
364
+ Visual schema definition for an asset.
365
+
366
+ You can import the `Visual` class directly from `nextmv`:
367
+
368
+ ```python
369
+ from nextmv import Visual
370
+ ```
371
+
372
+ This class defines how an asset is plotted in the Nextmv Console,
373
+ including the schema type, label, and display type.
374
+
375
+ Parameters
376
+ ----------
377
+ visual_schema : VisualSchema
378
+ Schema of the visual asset.
379
+ label : str
380
+ Label for the custom tab of the visual asset in the Nextmv Console.
381
+ visual_type : str, optional
382
+ Defines the type of custom visual. Default is "custom-tab".
383
+
384
+ Raises
385
+ ------
386
+ ValueError
387
+ If an unsupported schema or visual_type is provided.
388
+
389
+ Examples
390
+ --------
391
+ >>> from nextmv.output import Visual, VisualSchema
392
+ >>> visual = Visual(visual_schema=VisualSchema.CHARTJS, label="Performance Chart")
393
+ >>> visual.visual_schema
394
+ <VisualSchema.CHARTJS: 'chartjs'>
395
+ >>> visual.label
396
+ 'Performance Chart'
190
397
  """
191
398
 
192
399
  visual_schema: VisualSchema = Field(
@@ -207,6 +414,14 @@ class Visual(BaseModel):
207
414
  details."""
208
415
 
209
416
  def __post_init__(self):
417
+ """
418
+ Validate the visual schema and type.
419
+
420
+ Raises
421
+ ------
422
+ ValueError
423
+ If the visual_schema is not in VisualSchema or if visual_type is not 'custom-tab'.
424
+ """
210
425
  if self.visual_schema not in VisualSchema:
211
426
  raise ValueError(f"unsupported schema: {self.visual_schema}, supported schemas are {VisualSchema}")
212
427
 
@@ -216,7 +431,47 @@ class Visual(BaseModel):
216
431
 
217
432
  class Asset(BaseModel):
218
433
  """
219
- An asset represents downloadable information that is part of the `Output`.
434
+ Represents downloadable information that is part of the `Output`.
435
+
436
+ You can import the `Asset` class directly from `nextmv`:
437
+
438
+ ```python
439
+ from nextmv import Asset
440
+ ```
441
+
442
+ An asset contains content that can be serialized to JSON and optionally
443
+ includes visual information for rendering in the Nextmv Console.
444
+
445
+ Parameters
446
+ ----------
447
+ name : str
448
+ Name of the asset.
449
+ content : Any
450
+ Content of the asset. The type must be serializable to JSON.
451
+ content_type : str, optional
452
+ Content type of the asset. Only "json" is currently supported. Default is "json".
453
+ description : str, optional
454
+ Description of the asset. Default is None.
455
+ visual : Visual, optional
456
+ Visual schema of the asset. Default is None.
457
+
458
+ Raises
459
+ ------
460
+ ValueError
461
+ If the content_type is not "json".
462
+
463
+ Examples
464
+ --------
465
+ >>> from nextmv.output import Asset, Visual, VisualSchema
466
+ >>> visual = Visual(visual_schema=VisualSchema.CHARTJS, label="Solution Progress")
467
+ >>> asset = Asset(
468
+ ... name="optimization_progress",
469
+ ... content={"iterations": [1, 2, 3], "values": [10, 8, 7]},
470
+ ... description="Optimization progress over iterations",
471
+ ... visual=visual
472
+ ... )
473
+ >>> asset.name
474
+ 'optimization_progress'
220
475
  """
221
476
 
222
477
  name: str
@@ -232,6 +487,14 @@ class Asset(BaseModel):
232
487
  """Visual schema of the asset."""
233
488
 
234
489
  def __post_init__(self):
490
+ """
491
+ Validate the content type.
492
+
493
+ Raises
494
+ ------
495
+ ValueError
496
+ If the content_type is not "json".
497
+ """
235
498
  if self.content_type != "json":
236
499
  raise ValueError(f"unsupported content_type: {self.content_type}, supported types are `json`")
237
500
 
@@ -239,44 +502,86 @@ class Asset(BaseModel):
239
502
  @dataclass
240
503
  class Output:
241
504
  """
242
- Output of a decision problem. This class is used to be later be written to
243
- some location.
244
-
245
- The output can be in different formats, such as JSON (default) or
246
- CSV_ARCHIVE.
505
+ Output of a decision problem.
247
506
 
248
- If you used options, you can also include them in the output, to be
249
- serialized to the write location.
507
+ You can import the `Output` class directly from `nextmv`:
250
508
 
251
- The most important part of the output is the solution, which represents the
252
- result of the decision problem. The solution’s type must match the
253
- `output_format`:
509
+ ```python
510
+ from nextmv import Output
511
+ ```
254
512
 
255
- - `OutputFormat.JSON`: the data must be `dict[str, Any]`.
256
- - `OutputFormat.CSV_ARCHIVE`: the data must be `dict[str, list[dict[str,
257
- Any]]]`. The keys represent the file names where the data should be
258
- written. The values are lists of dictionaries, where each dictionary
259
- represents a row in the CSV file.
260
-
261
- The statistics are used to keep track of different metrics that were
262
- obtained after the run was completed. Although it can be a simple
263
- dictionary, we recommend using the `Statistics` class to ensure that the
264
- data is correctly formatted.
513
+ This class is used to structure the output of a decision problem that
514
+ can later be written to various destinations. It supports different output
515
+ formats and allows for customization of the serialization process.
265
516
 
266
517
  Parameters
267
518
  ----------
268
- options : Options, optional
269
- Options that the `Input` were created with.
270
- output_format : OutputFormat, optional
519
+ options : Optional[Union[Options, dict[str, Any]]], optional
520
+ Options that the `Output` was created with. These options can be of type
521
+ `Options` or a simple dictionary. Default is None.
522
+ output_format : Optional[OutputFormat], optional
271
523
  Format of the output data. Default is `OutputFormat.JSON`.
272
- solution : Union[dict[str, Any], dict[str, list[dict[str, Any]]], optional
273
- The solution to the decision problem.
274
- statistics : Union[Statistics, dict[str, Any], optional
275
- Statistics of the solution.
524
+ solution : Optional[Union[dict[str, Any], Any, dict[str, list[dict[str, Any]]]]], optional
525
+ The solution to the decision problem. The type must match the
526
+ `output_format`. Default is None.
527
+ statistics : Optional[Union[Statistics, dict[str, Any]]], optional
528
+ Statistics of the solution. Default is None.
529
+ csv_configurations : Optional[dict[str, Any]], optional
530
+ Configuration for writing CSV files. Default is None.
531
+ json_configurations : Optional[dict[str, Any]], optional
532
+ Configuration for writing JSON files. Default is None.
533
+ assets : Optional[list[Union[Asset, dict[str, Any]]]], optional
534
+ List of assets to be included in the output. Default is None.
535
+
536
+ Raises
537
+ ------
538
+ ValueError
539
+ If the solution is not compatible with the specified output_format.
540
+ TypeError
541
+ If options, statistics, or assets have unsupported types.
542
+
543
+ Notes
544
+ -----
545
+ The solution's type must match the `output_format`:
546
+
547
+ - `OutputFormat.JSON`: the data must be `dict[str, Any]` or `Any`.
548
+ - `OutputFormat.CSV_ARCHIVE`: the data must be `dict[str, list[dict[str, Any]]]`.
549
+ The keys represent the file names where the data should be written. The values
550
+ are lists of dictionaries, where each dictionary represents a row in the CSV file.
551
+
552
+ Examples
553
+ --------
554
+ >>> from nextmv.output import Output, OutputFormat, Statistics, RunStatistics
555
+ >>> run_stats = RunStatistics(duration=30.0, iterations=100)
556
+ >>> stats = Statistics(run=run_stats)
557
+ >>> solution = {"routes": [{"vehicle": 1, "stops": [1, 2, 3]}, {"vehicle": 2, "stops": [4, 5]}]}
558
+ >>> output = Output(
559
+ ... output_format=OutputFormat.JSON,
560
+ ... solution=solution,
561
+ ... statistics=stats,
562
+ ... json_configurations={"indent": 4}
563
+ ... )
564
+ >>> output_dict = output.to_dict()
565
+ >>> "solution" in output_dict and "statistics" in output_dict
566
+ True
276
567
  """
277
568
 
278
- options: Optional[Options] = None
279
- """Options that the `Input` were created with."""
569
+ options: Optional[Union[Options, dict[str, Any]]] = None
570
+ """
571
+ Options that the `Output` was created with. These options can be of type
572
+ `Options` or a simple dictionary. If the options are of type `Options`,
573
+ they will be serialized to a dictionary using the `to_dict` method. If
574
+ they are a dictionary, they will be used as is. If the options are not
575
+ provided, an empty dictionary will be used. If the options are of type
576
+ `dict`, then the dictionary should have the following structure:
577
+
578
+ ```python
579
+ {
580
+ "duration": "30",
581
+ "threads": 4,
582
+ }
583
+ ```
584
+ """
280
585
  output_format: Optional[OutputFormat] = OutputFormat.JSON
281
586
  """Format of the output data. Default is `OutputFormat.JSON`."""
282
587
  solution: Optional[
@@ -287,22 +592,47 @@ class Output:
287
592
  ] = None
288
593
  """The solution to the decision problem."""
289
594
  statistics: Optional[Union[Statistics, dict[str, Any]]] = None
290
- """Statistics of the solution."""
595
+ """
596
+ Statistics of the solution. These statistics can be of type `Statistics` or a
597
+ simple dictionary. If the statistics are of type `Statistics`, they will be
598
+ serialized to a dictionary using the `to_dict` method. If they are a
599
+ dictionary, they will be used as is. If the statistics are not provided, an
600
+ empty dictionary will be used.
601
+ """
291
602
  csv_configurations: Optional[dict[str, Any]] = None
292
- """Optional configuration for writing CSV files, to be used when the
293
- `output_format` is OutputFormat.CSV_ARCHIVE. These configurations are
294
- passed as kwargs to the `DictWriter` class from the `csv` module."""
603
+ """
604
+ Optional configuration for writing CSV files, to be used when the
605
+ `output_format` is `OutputFormat.CSV_ARCHIVE`. These configurations are
606
+ passed as kwargs to the `DictWriter` class from the `csv` module.
607
+ """
295
608
  json_configurations: Optional[dict[str, Any]] = None
296
- """Optional configuration for writing JSON files, to be used when the
297
- `output_format` is OutputFormat.JSON. These configurations are passed as
298
- kwargs to the `json.dumps` function."""
299
- assets: Optional[list[Asset]] = None
300
- """Optional list of assets to be included in the output."""
609
+ """
610
+ Optional configuration for writing JSON files, to be used when the
611
+ `output_format` is `OutputFormat.JSON`. These configurations are passed as
612
+ kwargs to the `json.dumps` function.
613
+ """
614
+ assets: Optional[list[Union[Asset, dict[str, Any]]]] = None
615
+ """
616
+ Optional list of assets to be included in the output. These assets can be of
617
+ type `Asset` or a simple dictionary. If the assets are of type `Asset`, they
618
+ will be serialized to a dictionary using the `to_dict` method. If they are a
619
+ dictionary, they will be used as is. If the assets are not provided, an
620
+ empty list will be used.
621
+ """
301
622
 
302
623
  def __post_init__(self):
303
- """Check that the solution matches the format given to initialize the
304
- class."""
624
+ """
625
+ Initialize and validate the Output instance.
626
+
627
+ This method performs two main tasks:
628
+ 1. Creates a deep copy of the options to preserve the original values
629
+ 2. Validates that the solution matches the specified output_format
305
630
 
631
+ Raises
632
+ ------
633
+ ValueError
634
+ If the solution is not compatible with the specified output_format.
635
+ """
306
636
  # Capture a snapshot of the options that were used to create the class
307
637
  # so even if they are changed later, we have a record of the original.
308
638
  init_options = self.options
@@ -327,68 +657,167 @@ class Output:
327
657
  "output_format OutputFormat.CSV_ARCHIVE, supported type is `dict`"
328
658
  )
329
659
 
330
- def to_dict(self) -> dict[str, any]:
660
+ def to_dict(self) -> dict[str, Any]: # noqa: C901
331
661
  """
332
662
  Convert the `Output` object to a dictionary.
333
663
 
334
664
  Returns
335
665
  -------
336
- dict[str, any]
666
+ dict[str, Any]
337
667
  The dictionary representation of the `Output` object.
338
668
  """
339
669
 
670
+ # Options need to end up as a dict, so we achieve that based on the
671
+ # type of options that were used to create the class.
672
+ if self.options is None:
673
+ options = {}
674
+ elif isinstance(self.options, Options):
675
+ options = self.options.to_dict()
676
+ elif isinstance(self.options, dict):
677
+ options = self.options
678
+ else:
679
+ raise TypeError(f"unsupported options type: {type(self.options)}, supported types are `Options` or `dict`")
680
+
681
+ # Statistics need to end up as a dict, so we achieve that based on the
682
+ # type of statistics that were used to create the class.
683
+ if self.statistics is None:
684
+ statistics = {}
685
+ elif isinstance(self.statistics, Statistics):
686
+ statistics = self.statistics.to_dict()
687
+ elif isinstance(self.statistics, dict):
688
+ statistics = self.statistics
689
+ else:
690
+ raise TypeError(
691
+ f"unsupported statistics type: {type(self.statistics)}, supported types are `Statistics` or `dict`"
692
+ )
693
+
694
+ # Assets need to end up as a list of dicts, so we achieve that based on
695
+ # the type of each asset in the list.
696
+ assets = []
697
+ if isinstance(self.assets, list):
698
+ for ix, asset in enumerate(self.assets):
699
+ if isinstance(asset, Asset):
700
+ assets.append(asset.to_dict())
701
+ elif isinstance(asset, dict):
702
+ assets.append(asset)
703
+ else:
704
+ raise TypeError(
705
+ f"unsupported asset {ix}, type: {type(asset)}; supported types are `Asset` or `dict`"
706
+ )
707
+ elif self.assets is not None:
708
+ raise TypeError(f"unsupported assets type: {type(self.assets)}, supported types are `list`")
709
+
340
710
  output_dict = {
341
- "options": self.options.to_dict() if self.options is not None else {},
711
+ "options": options,
342
712
  "solution": self.solution if self.solution is not None else {},
343
- "statistics": self.statistics.to_dict() if self.statistics is not None else {},
344
- "assets": [asset.to_dict() for asset in self.assets] if self.assets is not None else [],
713
+ "statistics": statistics,
714
+ "assets": assets,
345
715
  }
346
716
 
347
- if self.output_format == OutputFormat.CSV_ARCHIVE:
717
+ # Add the auxiliary configurations to the output dictionary if they are
718
+ # defined and not empty.
719
+ if (
720
+ self.output_format == OutputFormat.CSV_ARCHIVE
721
+ and self.csv_configurations is not None
722
+ and self.csv_configurations != {}
723
+ ):
348
724
  output_dict["csv_configurations"] = self.csv_configurations
349
- elif self.output_format == OutputFormat.JSON:
725
+ elif (
726
+ self.output_format == OutputFormat.JSON
727
+ and self.json_configurations is not None
728
+ and self.json_configurations != {}
729
+ ):
350
730
  output_dict["json_configurations"] = self.json_configurations
351
731
 
352
732
  return output_dict
353
733
 
354
734
 
355
735
  class OutputWriter:
356
- """Base class for writing outputs."""
736
+ """
737
+ Base class for writing outputs.
738
+
739
+ You can import the `OutputWriter` class directly from `nextmv`:
740
+
741
+ ```python
742
+ from nextmv import OutputWriter
743
+ ```
744
+
745
+ This is an abstract base class that defines the interface for writing outputs
746
+ to different destinations. Subclasses should implement the `write` method.
747
+
748
+ Examples
749
+ --------
750
+ >>> class CustomOutputWriter(OutputWriter):
751
+ ... def write(self, output, path=None, **kwargs):
752
+ ... # Custom implementation for writing output
753
+ ... print(f"Writing output to {path}")
754
+ """
357
755
 
358
756
  def write(self, output: Union[Output, dict[str, Any], BaseModel], *args, **kwargs) -> None:
359
757
  """
360
- Write the output data. This method should be implemented by subclasses.
361
- """
758
+ Write the output data.
759
+
760
+ This is an abstract method that should be implemented by subclasses.
761
+
762
+ Parameters
763
+ ----------
764
+ output : Union[Output, dict[str, Any], BaseModel]
765
+ The output data to write.
766
+ *args
767
+ Variable length argument list.
768
+ **kwargs
769
+ Arbitrary keyword arguments.
362
770
 
771
+ Raises
772
+ ------
773
+ NotImplementedError
774
+ This method must be implemented by subclasses.
775
+ """
363
776
  raise NotImplementedError
364
777
 
365
778
 
366
779
  class LocalOutputWriter(OutputWriter):
367
780
  """
368
- Class for write outputs to local files or stdout. Call the `write` method
369
- to write the output data.
781
+ Class for writing outputs to local files or stdout.
782
+
783
+ You can import the `LocalOutputWriter` class directly from `nextmv`:
784
+
785
+ ```python
786
+ from nextmv import LocalOutputWriter
787
+ ```
788
+
789
+ This class implements the OutputWriter interface to write output data to
790
+ local files or stdout. The destination and format depend on the output
791
+ format and the provided path.
792
+
793
+ Examples
794
+ --------
795
+ >>> from nextmv.output import LocalOutputWriter, Output, Statistics
796
+ >>> writer = LocalOutputWriter()
797
+ >>> output = Output(solution={"result": 42}, statistics=Statistics())
798
+ >>> # Write to stdout
799
+ >>> writer.write(output, path=None)
800
+ >>> # Write to a file
801
+ >>> writer.write(output, path="results.json")
370
802
  """
371
803
 
372
804
  def _write_json(
373
805
  output: Union[Output, dict[str, Any], BaseModel],
374
- options: dict[str, Any],
375
- statistics: dict[str, Any],
376
- assets: list[dict[str, Any]],
806
+ output_dict: dict[str, Any],
377
807
  path: Optional[str] = None,
378
808
  ) -> None:
379
- if isinstance(output, dict):
380
- final_output = output
381
- elif isinstance(output, BaseModel):
382
- final_output = output.to_dict()
383
- else:
384
- solution = output.solution if output.solution is not None else {}
385
- final_output = {
386
- "options": options,
387
- "solution": solution,
388
- "statistics": statistics,
389
- "assets": assets,
390
- }
809
+ """
810
+ Write output in JSON format.
391
811
 
812
+ Parameters
813
+ ----------
814
+ output : Union[Output, dict[str, Any], BaseModel]
815
+ The output object containing configuration.
816
+ output_dict : dict[str, Any]
817
+ Dictionary representation of the output to write.
818
+ path : str, optional
819
+ Path to write the output. If None or empty, writes to stdout.
820
+ """
392
821
  json_configurations = {}
393
822
  if hasattr(output, "json_configurations") and output.json_configurations is not None:
394
823
  json_configurations = output.json_configurations
@@ -402,7 +831,7 @@ class LocalOutputWriter(OutputWriter):
402
831
  del json_configurations["default"]
403
832
 
404
833
  serialized = json.dumps(
405
- final_output,
834
+ output_dict,
406
835
  indent=indent,
407
836
  default=custom_serial,
408
837
  **json_configurations,
@@ -416,12 +845,28 @@ class LocalOutputWriter(OutputWriter):
416
845
  file.write(serialized + "\n")
417
846
 
418
847
  def _write_archive(
419
- output: Output,
420
- options: dict[str, Any],
421
- statistics: dict[str, Any],
422
- assets: list[dict[str, Any]],
848
+ output: Union[Output, dict[str, Any], BaseModel],
849
+ output_dict: dict[str, Any],
423
850
  path: Optional[str] = None,
424
851
  ) -> None:
852
+ """
853
+ Write output in CSV archive format.
854
+
855
+ Parameters
856
+ ----------
857
+ output : Union[Output, dict[str, Any], BaseModel]
858
+ The output object containing configuration and solution data.
859
+ output_dict : dict[str, Any]
860
+ Dictionary representation of the output to write.
861
+ path : str, optional
862
+ Directory path to write the CSV files. If None or empty,
863
+ writes to a directory named "output" in the current working directory.
864
+
865
+ Raises
866
+ ------
867
+ ValueError
868
+ If the path is an existing file instead of a directory.
869
+ """
425
870
  dir_path = "output"
426
871
  if path is not None and path != "":
427
872
  if os.path.isfile(path):
@@ -434,9 +879,9 @@ class LocalOutputWriter(OutputWriter):
434
879
 
435
880
  serialized = json.dumps(
436
881
  {
437
- "options": options,
438
- "statistics": statistics,
439
- "assets": assets,
882
+ "options": output_dict.get("options", {}),
883
+ "statistics": output_dict.get("statistics", {}),
884
+ "assets": output_dict.get("assets", []),
440
885
  },
441
886
  indent=2,
442
887
  )
@@ -465,6 +910,7 @@ class LocalOutputWriter(OutputWriter):
465
910
  OutputFormat.JSON: _write_json,
466
911
  OutputFormat.CSV_ARCHIVE: _write_archive,
467
912
  }
913
+ """Dictionary mapping output formats to writer functions."""
468
914
 
469
915
  def write(
470
916
  self,
@@ -473,40 +919,48 @@ class LocalOutputWriter(OutputWriter):
473
919
  skip_stdout_reset: bool = False,
474
920
  ) -> None:
475
921
  """
476
- Write the `output` to the local filesystem. Consider the following for
477
- the `path` parameter, depending on the `Output.output_format`:
922
+ Write the output to the local filesystem or stdout.
478
923
 
479
- - `OutputFormat.JSON`: the `path` is the file where the JSON data will
480
- be written. If empty or `None`, the data will be written to stdout.
481
- - `OutputFormat.CSV_ARCHIVE`: the `path` is the directory where the CSV
482
- files will be written. If empty or `None`, the data will be written
483
- to a directory named `output` under the current working directory.
484
- The `Output.options` and `Output.statistics` will be written to
485
- stdout.
486
-
487
- This function detects if stdout was redirected and resets it to avoid
488
- unexpected behavior. If you want to skip this behavior, set the
489
- `skip_stdout_reset` parameter to `True`.
490
-
491
- If the `output` is a `dict`, it will be simply written to the specified
492
- `path`, as a passthrough. On the other hand, if the `output` is of type
493
- `Output`, a more structured object will be written, which adheres to
494
- the schema specified by the corresponding `Output` class.
924
+ This method writes the provided output to the specified path or to stdout,
925
+ depending on the output format and the path parameter.
495
926
 
496
927
  Parameters
497
928
  ----------
498
- output: Output, dict[str, Any]
499
- Output data to write.
500
- path : str
501
- Path to write the output data to.
929
+ output : Union[Output, dict[str, Any], BaseModel]
930
+ Output data to write. Can be an Output object, a dictionary, or a BaseModel.
931
+ path : str, optional
932
+ Path to write the output data to. The interpretation depends on the output format:
933
+ - For OutputFormat.JSON: File path for the JSON output. If None or empty, writes to stdout.
934
+ - For OutputFormat.CSV_ARCHIVE: Directory path for CSV files. If None or empty,
935
+ writes to a directory named "output" in the current working directory.
502
936
  skip_stdout_reset : bool, optional
503
- Skip resetting stdout before writing the output data. Default is
504
- `False`.
937
+ Skip resetting stdout before writing the output data. Default is False.
505
938
 
506
939
  Raises
507
940
  ------
508
941
  ValueError
509
- If the `Output.output_format` is not supported.
942
+ If the Output.output_format is not supported.
943
+ TypeError
944
+ If the output is of an unsupported type.
945
+
946
+ Notes
947
+ -----
948
+ This function detects if stdout was redirected and resets it to avoid
949
+ unexpected behavior. If you want to skip this behavior, set the
950
+ skip_stdout_reset parameter to True.
951
+
952
+ If the output is a dict or a BaseModel, it will be written as JSON. If
953
+ the output is an Output object, it will be written according to its
954
+ output_format.
955
+
956
+ Examples
957
+ --------
958
+ >>> from nextmv.output import LocalOutputWriter, Output
959
+ >>> writer = LocalOutputWriter()
960
+ >>> # Write JSON to a file
961
+ >>> writer.write(Output(solution={"result": 42}), path="result.json")
962
+ >>> # Write JSON to stdout
963
+ >>> writer.write({"simple": "data"})
510
964
  """
511
965
 
512
966
  # If the user forgot to reset stdout after redirecting it, we need to
@@ -525,88 +979,24 @@ class LocalOutputWriter(OutputWriter):
525
979
  f"unsupported output type: {type(output)}, supported types are `Output`, `dict`, `BaseModel`"
526
980
  )
527
981
 
528
- statistics = self._extract_statistics(output)
529
- options = self._extract_options(output)
530
- assets = self._extract_assets(output)
982
+ output_dict = {}
983
+ if isinstance(output, Output):
984
+ output_dict = output.to_dict()
985
+ elif isinstance(output, BaseModel):
986
+ output_dict = output.to_dict()
987
+ elif isinstance(output, dict):
988
+ output_dict = output
989
+ else:
990
+ raise TypeError(
991
+ f"unsupported output type: {type(output)}, supported types are `Output`, `dict`, `BaseModel`"
992
+ )
531
993
 
532
994
  self.FILE_WRITERS[output_format](
533
995
  output=output,
534
- options=options,
535
- statistics=statistics,
536
- assets=assets,
996
+ output_dict=output_dict,
537
997
  path=path,
538
998
  )
539
999
 
540
- @staticmethod
541
- def _extract_statistics(output: Union[Output, dict[str, Any]]) -> dict[str, Any]:
542
- """Extract JSON-serializable statistics."""
543
-
544
- statistics = {}
545
-
546
- if not isinstance(output, Output):
547
- return statistics
548
-
549
- stats = output.statistics
550
-
551
- if stats is None:
552
- return statistics
553
-
554
- if isinstance(stats, Statistics):
555
- statistics = stats.to_dict()
556
- elif isinstance(stats, dict):
557
- statistics = stats
558
- else:
559
- raise TypeError(f"unsupported statistics type: {type(stats)}, supported types are `Statistics` or `dict`")
560
-
561
- return statistics
562
-
563
- @staticmethod
564
- def _extract_options(output: Union[Output, dict[str, Any]]) -> dict[str, Any]:
565
- """Extract JSON-serializable options."""
566
-
567
- options = {}
568
-
569
- if not isinstance(output, Output):
570
- return options
571
-
572
- opt = output.options
573
-
574
- if opt is None:
575
- return options
576
-
577
- if isinstance(opt, Options):
578
- options = opt.to_dict()
579
- elif isinstance(opt, dict):
580
- options = opt
581
- else:
582
- raise TypeError(f"unsupported options type: {type(opt)}, supported types are `Options` or `dict`")
583
-
584
- return options
585
-
586
- @staticmethod
587
- def _extract_assets(output: Union[Output, dict[str, Any]]) -> list[dict[str, Any]]:
588
- """Extract JSON-serializable assets."""
589
-
590
- assets = []
591
-
592
- if not isinstance(output, Output):
593
- return assets
594
-
595
- assts = output.assets
596
-
597
- if assts is None:
598
- return assets
599
-
600
- for ix, asset in enumerate(assts):
601
- if isinstance(asset, Asset):
602
- assets.append(asset.to_dict())
603
- elif isinstance(asset, dict):
604
- assets.append(asset)
605
- else:
606
- raise TypeError(f"unsupported asset {ix}, type: {type(asset)}; supported types are `Asset` or `dict`")
607
-
608
- return assets
609
-
610
1000
 
611
1001
  def write_local(
612
1002
  output: Union[Output, dict[str, Any]],
@@ -614,42 +1004,50 @@ def write_local(
614
1004
  skip_stdout_reset: bool = False,
615
1005
  ) -> None:
616
1006
  """
617
- DEPRECATION WARNING
618
- ----------
619
- `write_local` is deprecated, use `write` instead.
1007
+ !!! warning
1008
+ `write_local` is deprecated, use `write` instead.
1009
+
1010
+ Write the output to the local filesystem or stdout.
620
1011
 
621
1012
  This is a convenience function for instantiating a `LocalOutputWriter` and
622
1013
  calling its `write` method.
623
1014
 
624
- Write the `output` to the local filesystem. Consider the following for the
625
- `path` parameter, depending on the `Output.output_format`:
626
-
627
- - `OutputFormat.JSON`: the `path` is the file where the JSON data will
628
- be written. If empty or `None`, the data will be written to stdout.
629
- - `OutputFormat.CSV_ARCHIVE`: the `path` is the directory where the CSV
630
- files will be written. If empty or `None`, the data will be written
631
- to a directory named `output` under the current working directory.
632
- The `Output.options` and `Output.statistics` will be written to
633
- stdout.
634
-
635
- This function detects if stdout was redirected and resets it to avoid
636
- unexpected behavior. If you want to skip this behavior, set the
637
- `skip_stdout_reset` parameter to `True`.
638
-
639
1015
  Parameters
640
1016
  ----------
641
- output : Output, dict[str, Any]
642
- Output data to write.
643
- path : str
644
- Path to write the output data to.
1017
+ output : Union[Output, dict[str, Any]]
1018
+ Output data to write. Can be an Output object or a dictionary.
1019
+ path : str, optional
1020
+ Path to write the output data to. The interpretation depends on the
1021
+ output format:
1022
+
1023
+ - For `OutputFormat.JSON`: File path for the JSON output. If None or
1024
+ empty, writes to stdout.
1025
+ - For `OutputFormat.CSV_ARCHIVE`: Directory path for CSV files. If None
1026
+ or empty, writes to a directory named "output" in the current working
1027
+ directory.
645
1028
  skip_stdout_reset : bool, optional
646
- Skip resetting stdout before writing the output data. Default is
647
- `False`.
1029
+ Skip resetting stdout before writing the output data. Default is False.
648
1030
 
649
1031
  Raises
650
1032
  ------
651
1033
  ValueError
652
- If the `Output.output_format` is not supported.
1034
+ If the Output.output_format is not supported.
1035
+ TypeError
1036
+ If the output is of an unsupported type.
1037
+
1038
+ Notes
1039
+ -----
1040
+ This function detects if stdout was redirected and resets it to avoid
1041
+ unexpected behavior. If you want to skip this behavior, set the
1042
+ skip_stdout_reset parameter to True.
1043
+
1044
+ Examples
1045
+ --------
1046
+ >>> from nextmv.output import write_local, Output
1047
+ >>> # Write JSON to a file
1048
+ >>> write_local(Output(solution={"result": 42}), path="result.json")
1049
+ >>> # Write JSON to stdout
1050
+ >>> write_local({"simple": "data"})
653
1051
  """
654
1052
 
655
1053
  deprecated(
@@ -662,6 +1060,7 @@ def write_local(
662
1060
 
663
1061
 
664
1062
  _LOCAL_OUTPUT_WRITER = LocalOutputWriter()
1063
+ """Default LocalOutputWriter instance used by the write function."""
665
1064
 
666
1065
 
667
1066
  def write(
@@ -671,46 +1070,78 @@ def write(
671
1070
  writer: Optional[OutputWriter] = _LOCAL_OUTPUT_WRITER,
672
1071
  ) -> None:
673
1072
  """
674
- This is a convenience function for writing an `Output`, i.e.: write the
675
- output to the specified destination. The `writer` is used to call the
676
- `.write` method. Note that the default writes is the `LocalOutputWriter`.
1073
+ Write the output to the specified destination.
677
1074
 
678
- Consider the following for the `path` parameter, depending on the
679
- `Output.output_format`:
1075
+ You can import the `write` function directly from `nextmv`:
680
1076
 
681
- - `OutputFormat.JSON`: the `path` is the file where the JSON data will
682
- be written. If empty or `None`, the data will be written to stdout.
683
- - `OutputFormat.CSV_ARCHIVE`: the `path` is the directory where the CSV
684
- files will be written. If empty or `None`, the data will be written
685
- to a directory named `output` under the current working directory.
686
- The `Output.options` and `Output.statistics` will be written to
687
- stdout.
1077
+ ```python
1078
+ from nextmv import write
1079
+ ```
688
1080
 
689
- This function detects if stdout was redirected and resets it to avoid
690
- unexpected behavior. If you want to skip this behavior, set the
691
- `skip_stdout_reset` parameter to `True`.
1081
+ This is a convenience function for writing output data using a provided writer.
1082
+ By default, it uses the `LocalOutputWriter` to write to files or stdout.
692
1083
 
693
1084
  Parameters
694
1085
  ----------
695
- output : Output, dict[str, Any]
696
- Output data to write.
697
- path : str
698
- Path to write the output data to.
1086
+ output : Union[Output, dict[str, Any], BaseModel]
1087
+ Output data to write. Can be an Output object, a dictionary, or a BaseModel.
1088
+ path : str, optional
1089
+ Path to write the output data to. The interpretation depends on the
1090
+ output format:
1091
+
1092
+ - For `OutputFormat.JSON`: File path for the JSON output. If None or
1093
+ empty, writes to stdout.
1094
+ - For `OutputFormat.CSV_ARCHIVE`: Directory path for CSV files. If None
1095
+ or empty, writes to a directory named "output" in the current working
1096
+ directory.
699
1097
  skip_stdout_reset : bool, optional
700
- Skip resetting stdout before writing the output data. Default is
701
- `False`.
1098
+ Skip resetting stdout before writing the output data. Default is False.
1099
+ writer : OutputWriter, optional
1100
+ The writer to use for writing the output. Default is a
1101
+ `LocalOutputWriter` instance.
702
1102
 
703
1103
  Raises
704
1104
  ------
705
1105
  ValueError
706
- If the `Output.output_format` is not supported.
1106
+ If the Output.output_format is not supported.
1107
+ TypeError
1108
+ If the output is of an unsupported type.
1109
+
1110
+ Examples
1111
+ --------
1112
+ >>> from nextmv.output import write, Output, OutputFormat
1113
+ >>> # Write JSON to a file
1114
+ >>> write(Output(solution={"result": 42}), path="result.json")
1115
+ >>> # Write CSV archive
1116
+ >>> data = {"vehicles": [{"id": 1, "capacity": 100}, {"id": 2, "capacity": 150}]}
1117
+ >>> write(Output(output_format=OutputFormat.CSV_ARCHIVE, solution=data), path="output_dir")
707
1118
  """
708
1119
 
709
1120
  writer.write(output, path, skip_stdout_reset)
710
1121
 
711
1122
 
712
- def _custom_serial(obj: Any):
713
- """JSON serializer for objects not serializable by default one."""
1123
+ def _custom_serial(obj: Any) -> str:
1124
+ """
1125
+ JSON serializer for objects not serializable by default json serializer.
1126
+
1127
+ This function provides custom serialization for datetime objects, converting
1128
+ them to ISO format strings.
1129
+
1130
+ Parameters
1131
+ ----------
1132
+ obj : Any
1133
+ The object to serialize.
1134
+
1135
+ Returns
1136
+ -------
1137
+ str
1138
+ The serialized representation of the object.
1139
+
1140
+ Raises
1141
+ ------
1142
+ TypeError
1143
+ If the object type is not supported for serialization.
1144
+ """
714
1145
 
715
1146
  if isinstance(obj, (datetime.datetime | datetime.date)):
716
1147
  return obj.isoformat()