nextmv 0.10.3.dev0__py3-none-any.whl → 0.35.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nextmv/__about__.py +1 -1
- nextmv/__entrypoint__.py +39 -0
- nextmv/__init__.py +57 -0
- nextmv/_serialization.py +96 -0
- nextmv/base_model.py +79 -9
- nextmv/cloud/__init__.py +71 -10
- nextmv/cloud/acceptance_test.py +888 -17
- nextmv/cloud/account.py +154 -10
- nextmv/cloud/application.py +3644 -437
- nextmv/cloud/batch_experiment.py +292 -33
- nextmv/cloud/client.py +354 -53
- nextmv/cloud/ensemble.py +247 -0
- nextmv/cloud/input_set.py +121 -4
- nextmv/cloud/instance.py +125 -0
- nextmv/cloud/package.py +474 -0
- nextmv/cloud/scenario.py +410 -0
- nextmv/cloud/secrets.py +234 -0
- nextmv/cloud/url.py +73 -0
- nextmv/cloud/version.py +174 -0
- nextmv/default_app/.gitignore +1 -0
- nextmv/default_app/README.md +32 -0
- nextmv/default_app/app.yaml +12 -0
- nextmv/default_app/input.json +5 -0
- nextmv/default_app/main.py +37 -0
- nextmv/default_app/requirements.txt +2 -0
- nextmv/default_app/src/__init__.py +0 -0
- nextmv/default_app/src/main.py +37 -0
- nextmv/default_app/src/visuals.py +36 -0
- nextmv/deprecated.py +47 -0
- nextmv/input.py +883 -78
- nextmv/local/__init__.py +5 -0
- nextmv/local/application.py +1263 -0
- nextmv/local/executor.py +1040 -0
- nextmv/local/geojson_handler.py +323 -0
- nextmv/local/local.py +97 -0
- nextmv/local/plotly_handler.py +61 -0
- nextmv/local/runner.py +274 -0
- nextmv/logger.py +80 -9
- nextmv/manifest.py +1472 -0
- nextmv/model.py +431 -0
- nextmv/options.py +968 -78
- nextmv/output.py +1363 -231
- nextmv/polling.py +287 -0
- nextmv/run.py +1623 -0
- nextmv/safe.py +145 -0
- nextmv/status.py +122 -0
- {nextmv-0.10.3.dev0.dist-info → nextmv-0.35.0.dist-info}/METADATA +51 -288
- nextmv-0.35.0.dist-info/RECORD +50 -0
- {nextmv-0.10.3.dev0.dist-info → nextmv-0.35.0.dist-info}/WHEEL +1 -1
- nextmv/cloud/status.py +0 -29
- nextmv/nextroute/__init__.py +0 -2
- nextmv/nextroute/check/__init__.py +0 -26
- nextmv/nextroute/check/schema.py +0 -141
- nextmv/nextroute/schema/__init__.py +0 -19
- nextmv/nextroute/schema/input.py +0 -52
- nextmv/nextroute/schema/location.py +0 -13
- nextmv/nextroute/schema/output.py +0 -136
- nextmv/nextroute/schema/stop.py +0 -61
- nextmv/nextroute/schema/vehicle.py +0 -68
- nextmv-0.10.3.dev0.dist-info/RECORD +0 -28
- {nextmv-0.10.3.dev0.dist-info → nextmv-0.35.0.dist-info}/licenses/LICENSE +0 -0
nextmv/output.py
CHANGED
|
@@ -1,48 +1,130 @@
|
|
|
1
|
-
"""
|
|
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
|
+
SolutionFile
|
|
25
|
+
Represents a solution to be written as a file.
|
|
26
|
+
VisualSchema
|
|
27
|
+
Enumeration of supported visualization schemas.
|
|
28
|
+
Visual
|
|
29
|
+
Visual schema definition for an asset.
|
|
30
|
+
Asset
|
|
31
|
+
Represents downloadable information that is part of the `Output`.
|
|
32
|
+
Output
|
|
33
|
+
A class for representing the output of a decision problem.
|
|
34
|
+
OutputWriter
|
|
35
|
+
Base class for writing outputs to different destinations.
|
|
36
|
+
LocalOutputWriter
|
|
37
|
+
Class for writing outputs to local files or stdout.
|
|
38
|
+
|
|
39
|
+
Functions
|
|
40
|
+
---------
|
|
41
|
+
write
|
|
42
|
+
Write the output to the specified destination.
|
|
43
|
+
|
|
44
|
+
Attributes
|
|
45
|
+
----------
|
|
46
|
+
ASSETS_KEY : str
|
|
47
|
+
Assets key constant used for identifying assets in the run output.
|
|
48
|
+
STATISTICS_KEY : str
|
|
49
|
+
Statistics key constant used for identifying statistics in the run output.
|
|
50
|
+
SOLUTIONS_KEY : str
|
|
51
|
+
Solutions key constant used for identifying solutions in the run output.
|
|
52
|
+
OUTPUTS_KEY : str
|
|
53
|
+
Outputs key constant used for identifying outputs in the run output.
|
|
54
|
+
"""
|
|
2
55
|
|
|
3
56
|
import copy
|
|
4
57
|
import csv
|
|
5
|
-
import datetime
|
|
6
|
-
import json
|
|
7
58
|
import os
|
|
8
59
|
import sys
|
|
60
|
+
from collections.abc import Callable
|
|
9
61
|
from dataclasses import dataclass
|
|
10
62
|
from enum import Enum
|
|
11
|
-
from typing import Any
|
|
63
|
+
from typing import Any
|
|
12
64
|
|
|
13
|
-
from pydantic import Field
|
|
65
|
+
from pydantic import AliasChoices, Field
|
|
14
66
|
|
|
67
|
+
from nextmv._serialization import serialize_json
|
|
15
68
|
from nextmv.base_model import BaseModel
|
|
69
|
+
from nextmv.deprecated import deprecated
|
|
16
70
|
from nextmv.logger import reset_stdout
|
|
17
71
|
from nextmv.options import Options
|
|
18
72
|
|
|
73
|
+
ASSETS_KEY = "assets"
|
|
74
|
+
"""
|
|
75
|
+
Assets key constant used for identifying assets in the run output.
|
|
76
|
+
"""
|
|
77
|
+
STATISTICS_KEY = "statistics"
|
|
78
|
+
"""
|
|
79
|
+
Statistics key constant used for identifying statistics in the run output.
|
|
80
|
+
"""
|
|
81
|
+
SOLUTIONS_KEY = "solutions"
|
|
82
|
+
"""
|
|
83
|
+
Solutions key constant used for identifying solutions in the run output.
|
|
84
|
+
"""
|
|
85
|
+
OUTPUTS_KEY = "outputs"
|
|
86
|
+
"""
|
|
87
|
+
Outputs key constant used for identifying outputs in the run output.
|
|
88
|
+
"""
|
|
89
|
+
|
|
19
90
|
|
|
20
91
|
class RunStatistics(BaseModel):
|
|
21
92
|
"""
|
|
22
93
|
Statistics about a general run.
|
|
23
94
|
|
|
95
|
+
You can import the `RunStatistics` class directly from `nextmv`:
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from nextmv import RunStatistics
|
|
99
|
+
```
|
|
100
|
+
|
|
24
101
|
Parameters
|
|
25
102
|
----------
|
|
26
103
|
duration : float, optional
|
|
27
104
|
Duration of the run in seconds.
|
|
28
105
|
iterations : int, optional
|
|
29
106
|
Number of iterations.
|
|
30
|
-
custom : Union[Any,
|
|
31
|
-
Custom statistics created by the user. Can normally expect a `
|
|
107
|
+
custom : Union[Any, dict[str, Any]], optional
|
|
108
|
+
Custom statistics created by the user. Can normally expect a `dict[str,
|
|
32
109
|
Any]`.
|
|
110
|
+
|
|
111
|
+
Examples
|
|
112
|
+
--------
|
|
113
|
+
>>> from nextmv.output import RunStatistics
|
|
114
|
+
>>> stats = RunStatistics(duration=10.5, iterations=100)
|
|
115
|
+
>>> stats.duration
|
|
116
|
+
10.5
|
|
117
|
+
>>> stats.custom = {"convergence": 0.001}
|
|
118
|
+
>>> stats.to_dict()
|
|
119
|
+
{'duration': 10.5, 'iterations': 100, 'custom': {'convergence': 0.001}}
|
|
33
120
|
"""
|
|
34
121
|
|
|
35
|
-
duration:
|
|
122
|
+
duration: float | None = None
|
|
36
123
|
"""Duration of the run in seconds."""
|
|
37
|
-
iterations:
|
|
124
|
+
iterations: int | None = None
|
|
38
125
|
"""Number of iterations."""
|
|
39
|
-
custom:
|
|
40
|
-
|
|
41
|
-
Any,
|
|
42
|
-
Dict[str, Any],
|
|
43
|
-
]
|
|
44
|
-
] = None
|
|
45
|
-
"""Custom statistics created by the user. Can normally expect a `Dict[str,
|
|
126
|
+
custom: Any | dict[str, Any] | None = None
|
|
127
|
+
"""Custom statistics created by the user. Can normally expect a `dict[str,
|
|
46
128
|
Any]`."""
|
|
47
129
|
|
|
48
130
|
|
|
@@ -50,34 +132,51 @@ class ResultStatistics(BaseModel):
|
|
|
50
132
|
"""
|
|
51
133
|
Statistics about a specific result.
|
|
52
134
|
|
|
135
|
+
You can import the `ResultStatistics` class directly from `nextmv`:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from nextmv import ResultStatistics
|
|
139
|
+
```
|
|
140
|
+
|
|
53
141
|
Parameters
|
|
54
142
|
----------
|
|
55
143
|
duration : float, optional
|
|
56
144
|
Duration of the run in seconds.
|
|
57
145
|
value : float, optional
|
|
58
146
|
Value of the result.
|
|
59
|
-
custom : Union[Any,
|
|
60
|
-
Custom statistics created by the user. Can normally expect a `
|
|
147
|
+
custom : Union[Any, dict[str, Any]], optional
|
|
148
|
+
Custom statistics created by the user. Can normally expect a `dict[str,
|
|
61
149
|
Any]`.
|
|
150
|
+
|
|
151
|
+
Examples
|
|
152
|
+
--------
|
|
153
|
+
>>> from nextmv.output import ResultStatistics
|
|
154
|
+
>>> result_stats = ResultStatistics(duration=5.2, value=42.0)
|
|
155
|
+
>>> result_stats.value
|
|
156
|
+
42.0
|
|
157
|
+
>>> result_stats.custom = {"gap": 0.05}
|
|
158
|
+
>>> result_stats.to_dict()
|
|
159
|
+
{'duration': 5.2, 'value': 42.0, 'custom': {'gap': 0.05}}
|
|
62
160
|
"""
|
|
63
161
|
|
|
64
|
-
duration:
|
|
162
|
+
duration: float | None = None
|
|
65
163
|
"""Duration of the run in seconds."""
|
|
66
|
-
value:
|
|
164
|
+
value: float | None = None
|
|
67
165
|
"""Value of the result."""
|
|
68
|
-
custom:
|
|
69
|
-
|
|
70
|
-
Any,
|
|
71
|
-
Dict[str, Any],
|
|
72
|
-
]
|
|
73
|
-
] = None
|
|
74
|
-
"""Custom statistics created by the user. Can normally expect a `Dict[str,
|
|
166
|
+
custom: Any | dict[str, Any] | None = None
|
|
167
|
+
"""Custom statistics created by the user. Can normally expect a `dict[str,
|
|
75
168
|
Any]`."""
|
|
76
169
|
|
|
77
170
|
|
|
78
171
|
class DataPoint(BaseModel):
|
|
79
172
|
"""
|
|
80
|
-
A data point.
|
|
173
|
+
A data point representing a 2D coordinate.
|
|
174
|
+
|
|
175
|
+
You can import the `DataPoint` class directly from `nextmv`:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
from nextmv import DataPoint
|
|
179
|
+
```
|
|
81
180
|
|
|
82
181
|
Parameters
|
|
83
182
|
----------
|
|
@@ -85,6 +184,15 @@ class DataPoint(BaseModel):
|
|
|
85
184
|
X coordinate of the data point.
|
|
86
185
|
y : float
|
|
87
186
|
Y coordinate of the data point.
|
|
187
|
+
|
|
188
|
+
Examples
|
|
189
|
+
--------
|
|
190
|
+
>>> from nextmv.output import DataPoint
|
|
191
|
+
>>> point = DataPoint(x=3.5, y=4.2)
|
|
192
|
+
>>> point.x
|
|
193
|
+
3.5
|
|
194
|
+
>>> point.to_dict()
|
|
195
|
+
{'x': 3.5, 'y': 4.2}
|
|
88
196
|
"""
|
|
89
197
|
|
|
90
198
|
x: float
|
|
@@ -95,43 +203,83 @@ class DataPoint(BaseModel):
|
|
|
95
203
|
|
|
96
204
|
class Series(BaseModel):
|
|
97
205
|
"""
|
|
98
|
-
A series of data points.
|
|
206
|
+
A series of data points for visualization or analysis.
|
|
207
|
+
|
|
208
|
+
You can import the `Series` class directly from `nextmv`:
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
from nextmv import Series
|
|
212
|
+
```
|
|
99
213
|
|
|
100
214
|
Parameters
|
|
101
215
|
----------
|
|
102
216
|
name : str, optional
|
|
103
217
|
Name of the series.
|
|
104
|
-
data_points :
|
|
105
|
-
Data of the series.
|
|
218
|
+
data_points : list[DataPoint], optional
|
|
219
|
+
Data points of the series.
|
|
220
|
+
|
|
221
|
+
Examples
|
|
222
|
+
--------
|
|
223
|
+
>>> from nextmv.output import Series, DataPoint
|
|
224
|
+
>>> points = [DataPoint(x=1.0, y=2.0), DataPoint(x=2.0, y=3.0)]
|
|
225
|
+
>>> series = Series(name="Example Series", data_points=points)
|
|
226
|
+
>>> series.name
|
|
227
|
+
'Example Series'
|
|
228
|
+
>>> len(series.data_points)
|
|
229
|
+
2
|
|
106
230
|
"""
|
|
107
231
|
|
|
108
|
-
name:
|
|
232
|
+
name: str | None = None
|
|
109
233
|
"""Name of the series."""
|
|
110
|
-
data_points:
|
|
234
|
+
data_points: list[DataPoint] | None = None
|
|
111
235
|
"""Data of the series."""
|
|
112
236
|
|
|
113
237
|
|
|
114
238
|
class SeriesData(BaseModel):
|
|
115
239
|
"""
|
|
116
|
-
Data of
|
|
240
|
+
Data container for multiple series of data points.
|
|
241
|
+
|
|
242
|
+
You can import the `SeriesData` class directly from `nextmv`:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from nextmv import SeriesData
|
|
246
|
+
```
|
|
117
247
|
|
|
118
248
|
Parameters
|
|
119
249
|
----------
|
|
120
250
|
value : Series, optional
|
|
121
251
|
A series for the value of the solution.
|
|
122
|
-
custom :
|
|
252
|
+
custom : list[Series], optional
|
|
123
253
|
A list of series for custom statistics.
|
|
254
|
+
|
|
255
|
+
Examples
|
|
256
|
+
--------
|
|
257
|
+
>>> from nextmv.output import SeriesData, Series, DataPoint
|
|
258
|
+
>>> value_series = Series(name="Solution Value", data_points=[DataPoint(x=0, y=10), DataPoint(x=1, y=5)])
|
|
259
|
+
>>> custom_series = [Series(name="Gap", data_points=[DataPoint(x=0, y=0.5), DataPoint(x=1, y=0.1)])]
|
|
260
|
+
>>> series_data = SeriesData(value=value_series, custom=custom_series)
|
|
261
|
+
>>> series_data.value.name
|
|
262
|
+
'Solution Value'
|
|
263
|
+
>>> len(series_data.custom)
|
|
264
|
+
1
|
|
124
265
|
"""
|
|
125
266
|
|
|
126
|
-
value:
|
|
267
|
+
value: Series | None = None
|
|
127
268
|
"""A series for the value of the solution."""
|
|
128
|
-
custom:
|
|
269
|
+
custom: list[Series] | None = None
|
|
129
270
|
"""A list of series for custom statistics."""
|
|
130
271
|
|
|
131
272
|
|
|
132
273
|
class Statistics(BaseModel):
|
|
133
274
|
"""
|
|
134
|
-
|
|
275
|
+
Complete statistics container for a solution, including run metrics and
|
|
276
|
+
result data.
|
|
277
|
+
|
|
278
|
+
You can import the `Statistics` class directly from `nextmv`:
|
|
279
|
+
|
|
280
|
+
```python
|
|
281
|
+
from nextmv import Statistics
|
|
282
|
+
```
|
|
135
283
|
|
|
136
284
|
Parameters
|
|
137
285
|
----------
|
|
@@ -143,149 +291,935 @@ class Statistics(BaseModel):
|
|
|
143
291
|
Series data about some metric.
|
|
144
292
|
statistics_schema : str, optional
|
|
145
293
|
Schema (version). This class only supports `v1`.
|
|
294
|
+
|
|
295
|
+
Examples
|
|
296
|
+
--------
|
|
297
|
+
>>> from nextmv.output import Statistics, RunStatistics, ResultStatistics
|
|
298
|
+
>>> run_stats = RunStatistics(duration=10.0, iterations=50)
|
|
299
|
+
>>> result_stats = ResultStatistics(value=100.0)
|
|
300
|
+
>>> stats = Statistics(run=run_stats, result=result_stats, statistics_schema="v1")
|
|
301
|
+
>>> stats.run.duration
|
|
302
|
+
10.0
|
|
303
|
+
>>> stats.result.value
|
|
304
|
+
100.0
|
|
146
305
|
"""
|
|
147
306
|
|
|
148
|
-
run:
|
|
307
|
+
run: RunStatistics | None = None
|
|
149
308
|
"""Statistics about the run."""
|
|
150
|
-
result:
|
|
309
|
+
result: ResultStatistics | None = None
|
|
151
310
|
"""Statistics about the last result."""
|
|
152
|
-
series_data:
|
|
311
|
+
series_data: SeriesData | None = None
|
|
153
312
|
"""Data of the series."""
|
|
154
|
-
statistics_schema:
|
|
313
|
+
statistics_schema: str | None = Field(
|
|
314
|
+
serialization_alias="schema",
|
|
315
|
+
validation_alias=AliasChoices("schema", "statistics_schema"),
|
|
316
|
+
default="v1",
|
|
317
|
+
)
|
|
155
318
|
"""Schema (version). This class only supports `v1`."""
|
|
156
319
|
|
|
157
320
|
|
|
321
|
+
class VisualSchema(str, Enum):
|
|
322
|
+
"""
|
|
323
|
+
Enumeration of supported visualization schemas.
|
|
324
|
+
|
|
325
|
+
You can import the `VisualSchema` class directly from `nextmv`:
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
from nextmv import VisualSchema
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
This enum defines the different visualization libraries or rendering methods
|
|
332
|
+
that can be used to display custom asset data in the Nextmv Console.
|
|
333
|
+
|
|
334
|
+
Attributes
|
|
335
|
+
----------
|
|
336
|
+
CHARTJS : str
|
|
337
|
+
Tells Nextmv Console to render the custom asset data with the Chart.js library.
|
|
338
|
+
GEOJSON : str
|
|
339
|
+
Tells Nextmv Console to render the custom asset data as GeoJSON on a map.
|
|
340
|
+
PLOTLY : str
|
|
341
|
+
Tells Nextmv Console to render the custom asset data with the Plotly library.
|
|
342
|
+
"""
|
|
343
|
+
|
|
344
|
+
CHARTJS = "chartjs"
|
|
345
|
+
"""Tells Nextmv Console to render the custom asset data with the Chart.js
|
|
346
|
+
library."""
|
|
347
|
+
GEOJSON = "geojson"
|
|
348
|
+
"""Tells Nextmv Console to render the custom asset data as GeoJSON on a
|
|
349
|
+
map."""
|
|
350
|
+
PLOTLY = "plotly"
|
|
351
|
+
"""Tells Nextmv Console to render the custom asset data with the Plotly
|
|
352
|
+
library."""
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
class Visual(BaseModel):
|
|
356
|
+
"""
|
|
357
|
+
Visual schema definition for an asset.
|
|
358
|
+
|
|
359
|
+
You can import the `Visual` class directly from `nextmv`:
|
|
360
|
+
|
|
361
|
+
```python
|
|
362
|
+
from nextmv import Visual
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
This class defines how an asset is plotted in the Nextmv Console,
|
|
366
|
+
including the schema type, label, and display type.
|
|
367
|
+
|
|
368
|
+
Parameters
|
|
369
|
+
----------
|
|
370
|
+
visual_schema : VisualSchema
|
|
371
|
+
Schema of the visual asset.
|
|
372
|
+
label : str
|
|
373
|
+
Label for the custom tab of the visual asset in the Nextmv Console.
|
|
374
|
+
visual_type : str, optional
|
|
375
|
+
Defines the type of custom visual. Default is "custom-tab".
|
|
376
|
+
|
|
377
|
+
Raises
|
|
378
|
+
------
|
|
379
|
+
ValueError
|
|
380
|
+
If an unsupported schema or visual_type is provided.
|
|
381
|
+
|
|
382
|
+
Examples
|
|
383
|
+
--------
|
|
384
|
+
>>> from nextmv.output import Visual, VisualSchema
|
|
385
|
+
>>> visual = Visual(visual_schema=VisualSchema.CHARTJS, label="Performance Chart")
|
|
386
|
+
>>> visual.visual_schema
|
|
387
|
+
<VisualSchema.CHARTJS: 'chartjs'>
|
|
388
|
+
>>> visual.label
|
|
389
|
+
'Performance Chart'
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
visual_schema: VisualSchema = Field(
|
|
393
|
+
serialization_alias="schema",
|
|
394
|
+
validation_alias=AliasChoices("schema", "visual_schema"),
|
|
395
|
+
)
|
|
396
|
+
"""Schema of the visual asset."""
|
|
397
|
+
label: str
|
|
398
|
+
"""Label for the custom tab of the visual asset in the Nextmv Console."""
|
|
399
|
+
|
|
400
|
+
visual_type: str | None = Field(
|
|
401
|
+
serialization_alias="type",
|
|
402
|
+
validation_alias=AliasChoices("type", "visual_type"),
|
|
403
|
+
default="custom-tab",
|
|
404
|
+
)
|
|
405
|
+
"""Defines the type of custom visual, currently there is only one type:
|
|
406
|
+
`custom-tab`. This renders the visual in its own tab view of the run
|
|
407
|
+
details."""
|
|
408
|
+
|
|
409
|
+
def __post_init__(self):
|
|
410
|
+
"""
|
|
411
|
+
Validate the visual schema and type.
|
|
412
|
+
|
|
413
|
+
Raises
|
|
414
|
+
------
|
|
415
|
+
ValueError
|
|
416
|
+
If the visual_schema is not in VisualSchema or if visual_type is not 'custom-tab'.
|
|
417
|
+
"""
|
|
418
|
+
if self.visual_schema not in VisualSchema:
|
|
419
|
+
raise ValueError(f"unsupported schema: {self.visual_schema}, supported schemas are {VisualSchema}")
|
|
420
|
+
|
|
421
|
+
if self.visual_type != "custom-tab":
|
|
422
|
+
raise ValueError(f"unsupported visual_type: {self.visual_type}, supported types are `custom-tab`")
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
class Asset(BaseModel):
|
|
426
|
+
"""
|
|
427
|
+
Represents downloadable information that is part of the `Output`.
|
|
428
|
+
|
|
429
|
+
You can import the `Asset` class directly from `nextmv`:
|
|
430
|
+
|
|
431
|
+
```python
|
|
432
|
+
from nextmv import Asset
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
An asset contains content that can be serialized to JSON and optionally
|
|
436
|
+
includes visual information for rendering in the Nextmv Console.
|
|
437
|
+
|
|
438
|
+
Parameters
|
|
439
|
+
----------
|
|
440
|
+
name : str
|
|
441
|
+
Name of the asset.
|
|
442
|
+
content : Any
|
|
443
|
+
Content of the asset. The type must be serializable to JSON.
|
|
444
|
+
content_type : str, optional
|
|
445
|
+
Content type of the asset. Only "json" is currently supported. Default is "json".
|
|
446
|
+
description : str, optional
|
|
447
|
+
Description of the asset. Default is None.
|
|
448
|
+
visual : Visual, optional
|
|
449
|
+
Visual schema of the asset. Default is None.
|
|
450
|
+
|
|
451
|
+
Raises
|
|
452
|
+
------
|
|
453
|
+
ValueError
|
|
454
|
+
If the content_type is not "json".
|
|
455
|
+
|
|
456
|
+
Examples
|
|
457
|
+
--------
|
|
458
|
+
>>> from nextmv.output import Asset, Visual, VisualSchema
|
|
459
|
+
>>> visual = Visual(visual_schema=VisualSchema.CHARTJS, label="Solution Progress")
|
|
460
|
+
>>> asset = Asset(
|
|
461
|
+
... name="optimization_progress",
|
|
462
|
+
... content={"iterations": [1, 2, 3], "values": [10, 8, 7]},
|
|
463
|
+
... description="Optimization progress over iterations",
|
|
464
|
+
... visual=visual
|
|
465
|
+
... )
|
|
466
|
+
>>> asset.name
|
|
467
|
+
'optimization_progress'
|
|
468
|
+
"""
|
|
469
|
+
|
|
470
|
+
name: str
|
|
471
|
+
"""Name of the asset."""
|
|
472
|
+
content: Any
|
|
473
|
+
"""Content of the asset. The type must be serializable to JSON."""
|
|
474
|
+
|
|
475
|
+
content_type: str | None = "json"
|
|
476
|
+
"""Content type of the asset. Only `json` is allowed"""
|
|
477
|
+
description: str | None = None
|
|
478
|
+
"""Description of the asset."""
|
|
479
|
+
visual: Visual | None = None
|
|
480
|
+
"""Visual schema of the asset."""
|
|
481
|
+
|
|
482
|
+
def __post_init__(self):
|
|
483
|
+
"""
|
|
484
|
+
Validate the content type.
|
|
485
|
+
|
|
486
|
+
Raises
|
|
487
|
+
------
|
|
488
|
+
ValueError
|
|
489
|
+
If the content_type is not "json".
|
|
490
|
+
"""
|
|
491
|
+
if self.content_type != "json":
|
|
492
|
+
raise ValueError(f"unsupported content_type: {self.content_type}, supported types are `json`")
|
|
493
|
+
|
|
494
|
+
|
|
158
495
|
class OutputFormat(str, Enum):
|
|
159
|
-
"""
|
|
496
|
+
"""
|
|
497
|
+
Enumeration of supported output formats.
|
|
160
498
|
|
|
161
|
-
|
|
499
|
+
You can import the `OutputFormat` class directly from `nextmv`:
|
|
500
|
+
|
|
501
|
+
```python
|
|
502
|
+
from nextmv import OutputFormat
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
This enum defines the different formats that can be used for outputting data.
|
|
506
|
+
Each format has specific requirements and behaviors when writing.
|
|
507
|
+
|
|
508
|
+
Attributes
|
|
509
|
+
----------
|
|
510
|
+
JSON : str
|
|
511
|
+
JSON format, utf-8 encoded.
|
|
512
|
+
CSV_ARCHIVE : str
|
|
513
|
+
CSV archive format: multiple CSV files.
|
|
514
|
+
MULTI_FILE : str
|
|
515
|
+
Multi-file format: multiple files in a directory.
|
|
516
|
+
TEXT : str
|
|
517
|
+
Text format, utf-8 encoded.
|
|
518
|
+
"""
|
|
519
|
+
|
|
520
|
+
JSON = "json"
|
|
162
521
|
"""JSON format, utf-8 encoded."""
|
|
163
|
-
CSV_ARCHIVE = "
|
|
522
|
+
CSV_ARCHIVE = "csv-archive"
|
|
164
523
|
"""CSV archive format: multiple CSV files."""
|
|
524
|
+
MULTI_FILE = "multi-file"
|
|
525
|
+
"""Multi-file format: multiple files in a directory."""
|
|
526
|
+
TEXT = "text"
|
|
527
|
+
"""Text format, utf-8 encoded."""
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
@dataclass
|
|
531
|
+
class SolutionFile:
|
|
532
|
+
"""
|
|
533
|
+
Represents a solution to be written as a file.
|
|
534
|
+
|
|
535
|
+
You can import the `SolutionFile` class directly from `nextmv`:
|
|
536
|
+
|
|
537
|
+
```python
|
|
538
|
+
from nextmv import SolutionFile
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
This class is used to define a solution that will be written to a file in
|
|
542
|
+
the filesystem. It includes the name of the file, the data to be written,
|
|
543
|
+
and the writer function that will handle the serialization of the data.
|
|
544
|
+
This `SolutionFile` class is typically used in the `Output`, when the
|
|
545
|
+
`Output.output_format` is set to `OutputFormat.MULTI_FILE`. Given that it
|
|
546
|
+
is difficult to handle every edge case of how a solution is serialized, and
|
|
547
|
+
written to a file, this class exists so that the user can implement the
|
|
548
|
+
`writer` callable of their choice and provide it with any `writer_args`
|
|
549
|
+
and `writer_kwargs` they might need.
|
|
550
|
+
|
|
551
|
+
Parameters
|
|
552
|
+
----------
|
|
553
|
+
name : str
|
|
554
|
+
Name of the output file. The file extension should be included in the
|
|
555
|
+
name.
|
|
556
|
+
data : Any
|
|
557
|
+
The actual data that will be written to the file. This can be any type
|
|
558
|
+
that can be given to the `writer` function. For example, if the `writer`
|
|
559
|
+
is a `csv.DictWriter`, then the data should be a list of dictionaries,
|
|
560
|
+
where each dictionary represents a row in the CSV file.
|
|
561
|
+
writer : Callable
|
|
562
|
+
Callable that writes the solution data to the file. This should be a
|
|
563
|
+
function implemented by the user. There are convenience functions that you
|
|
564
|
+
can use as a writer as well. The `writer` must receive, at the very
|
|
565
|
+
minimum, the following arguments:
|
|
566
|
+
|
|
567
|
+
- `file_path`: a `str` argument which is the location where this solution
|
|
568
|
+
will be written to. This includes the dir and the name of the file. As
|
|
569
|
+
such, the `name` parameter of this class is going to be passed to this
|
|
570
|
+
function joined with the directory where the file will be written.
|
|
571
|
+
- `data`: the actual data that will be written to the file. This can be any
|
|
572
|
+
type that can be given to the `writer` function. The `data` parameter of
|
|
573
|
+
this class is going to be passed to the `writer` function.
|
|
574
|
+
|
|
575
|
+
The `writer` can also receive additional arguments, and keyword arguments.
|
|
576
|
+
The `writer_args` and `writer_kwargs` parameters of this class can be used
|
|
577
|
+
to provide those additional arguments.
|
|
578
|
+
writer_args : Optional[list[Any]], optional
|
|
579
|
+
Positional arguments to pass to the writer function.
|
|
580
|
+
writer_kwargs : Optional[dict[str, Any]], optional
|
|
581
|
+
Keyword arguments to pass to the writer function.
|
|
582
|
+
|
|
583
|
+
Examples
|
|
584
|
+
--------
|
|
585
|
+
>>> from nextmv import SolutionFile
|
|
586
|
+
>>> solution_file = SolutionFile(
|
|
587
|
+
... name="solution.csv",
|
|
588
|
+
... data=[{"id": 1, "value": 100}, {"id": 2, "value": 200}],
|
|
589
|
+
... writer=csv.DictWriter,
|
|
590
|
+
... writer_kwargs={"fieldnames": ["id", "value"]},
|
|
591
|
+
... writer_args=[open("solution.csv", "w", newline="")],
|
|
592
|
+
... )
|
|
593
|
+
"""
|
|
594
|
+
|
|
595
|
+
name: str
|
|
596
|
+
"""
|
|
597
|
+
Name of the solution (output) file. The file extension should be included in the
|
|
598
|
+
name.
|
|
599
|
+
"""
|
|
600
|
+
data: Any
|
|
601
|
+
"""
|
|
602
|
+
The actual data that will be written to the file. This can be any type that
|
|
603
|
+
can be given to the `writer` function. For example, if the `writer` is a
|
|
604
|
+
`csv.DictWriter`, then the data should be a list of dictionaries, where
|
|
605
|
+
each dictionary represents a row in the CSV file.
|
|
606
|
+
"""
|
|
607
|
+
writer: Callable[[str, Any], None]
|
|
608
|
+
"""
|
|
609
|
+
Callable that writes the solution data to the file. This should be a
|
|
610
|
+
function implemented by the user. There are convenience functions that you
|
|
611
|
+
can use as a writer as well. The `writer` must receive, at the very
|
|
612
|
+
minimum, the following arguments:
|
|
613
|
+
|
|
614
|
+
- `file_path`: a `str` argument which is the location where this solution
|
|
615
|
+
will be written to. This includes the dir and the name of the file. As
|
|
616
|
+
such, the `name` parameter of this class is going to be passed to this
|
|
617
|
+
function joined with the directory where the file will be written.
|
|
618
|
+
- `data`: the actual data that will be written to the file. This can be any
|
|
619
|
+
type that can be given to the `writer` function. The `data` parameter of
|
|
620
|
+
this class is going to be passed to the `writer` function.
|
|
621
|
+
|
|
622
|
+
The `writer` can also receive additional arguments, and keyword arguments.
|
|
623
|
+
The `writer_args` and `writer_kwargs` parameters of this class can be used
|
|
624
|
+
to provide those additional arguments.
|
|
625
|
+
"""
|
|
626
|
+
writer_args: list[Any] | None = None
|
|
627
|
+
"""
|
|
628
|
+
Optional positional arguments to pass to the writer function. This can be
|
|
629
|
+
used to customize the behavior of the writer.
|
|
630
|
+
"""
|
|
631
|
+
writer_kwargs: dict[str, Any] | None = None
|
|
632
|
+
"""
|
|
633
|
+
Optional keyword arguments to pass to the writer function. This can be used
|
|
634
|
+
to customize the behavior of the writer.
|
|
635
|
+
"""
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def json_solution_file(
|
|
639
|
+
name: str,
|
|
640
|
+
data: dict[str, Any],
|
|
641
|
+
json_configurations: dict[str, Any] | None = None,
|
|
642
|
+
) -> SolutionFile:
|
|
643
|
+
"""
|
|
644
|
+
This is a convenience function to build a `SolutionFile`. It writes the
|
|
645
|
+
given `data` to a `.json` file with the provided `name`.
|
|
646
|
+
|
|
647
|
+
You can import this function directly from `nextmv`:
|
|
648
|
+
|
|
649
|
+
```python
|
|
650
|
+
from nextmv import json_solution_file
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
Parameters
|
|
654
|
+
----------
|
|
655
|
+
name : str
|
|
656
|
+
Name of the output file. You don't need to include the `.json`
|
|
657
|
+
extension.
|
|
658
|
+
data : dict[str, Any]
|
|
659
|
+
The actual data that will be written to the file. This should be a
|
|
660
|
+
dictionary that can be serialized to JSON.
|
|
661
|
+
json_configurations : Optional[dict[str, Any]], optional
|
|
662
|
+
Optional configuration options for the JSON serialization process. You
|
|
663
|
+
can use these options to configure parameters such as indentation.
|
|
664
|
+
|
|
665
|
+
Returns
|
|
666
|
+
-------
|
|
667
|
+
SolutionFile
|
|
668
|
+
The constructed `SolutionFile` object.
|
|
669
|
+
|
|
670
|
+
Examples
|
|
671
|
+
--------
|
|
672
|
+
>>> from nextmv import json_solution_file
|
|
673
|
+
>>> solution_file = json_solution_file(
|
|
674
|
+
... name="solution",
|
|
675
|
+
... data={"id": 1, "value": 100}
|
|
676
|
+
... )
|
|
677
|
+
>>> solution_file.name
|
|
678
|
+
'solution.json'
|
|
679
|
+
>>> solution_file.data
|
|
680
|
+
{'id': 1, 'value': 100}
|
|
681
|
+
"""
|
|
682
|
+
|
|
683
|
+
if not name.endswith(".json"):
|
|
684
|
+
name += ".json"
|
|
685
|
+
|
|
686
|
+
json_configurations = json_configurations or {}
|
|
687
|
+
|
|
688
|
+
def writer(file_path: str, write_data: dict[str, Any]) -> None:
|
|
689
|
+
serialized = serialize_json(write_data, json_configurations=json_configurations)
|
|
690
|
+
|
|
691
|
+
with open(file_path, "w", encoding="utf-8") as file:
|
|
692
|
+
file.write(serialized + "\n")
|
|
693
|
+
|
|
694
|
+
return SolutionFile(
|
|
695
|
+
name=name,
|
|
696
|
+
data=data,
|
|
697
|
+
writer=writer,
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def csv_solution_file(
|
|
702
|
+
name: str,
|
|
703
|
+
data: list[dict[str, Any]],
|
|
704
|
+
csv_configurations: dict[str, Any] | None = None,
|
|
705
|
+
) -> SolutionFile:
|
|
706
|
+
"""
|
|
707
|
+
This is a convenience function to build a `SolutionFile`. It writes the
|
|
708
|
+
given `data` to a `.csv` file with the provided `name`.
|
|
709
|
+
|
|
710
|
+
You can import this function directly from `nextmv`:
|
|
711
|
+
|
|
712
|
+
```python
|
|
713
|
+
from nextmv import csv_solution_file
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
Parameters
|
|
717
|
+
----------
|
|
718
|
+
name : str
|
|
719
|
+
Name of the output file. You don't need to include the `.csv`
|
|
720
|
+
extension.
|
|
721
|
+
data : list[dict[str, Any]]
|
|
722
|
+
The actual data that will be written to the file. This should be a list
|
|
723
|
+
of dictionaries, where each dictionary represents a row in the CSV file.
|
|
724
|
+
The keys of the dictionaries will be used as the column headers in the
|
|
725
|
+
CSV file.
|
|
726
|
+
csv_configurations : Optional[dict[str, Any]], optional
|
|
727
|
+
Optional configuration options for the CSV serialization process.
|
|
728
|
+
|
|
729
|
+
Returns
|
|
730
|
+
-------
|
|
731
|
+
SolutionFile
|
|
732
|
+
The constructed `SolutionFile` object.
|
|
733
|
+
|
|
734
|
+
Examples
|
|
735
|
+
--------
|
|
736
|
+
>>> from nextmv import csv_solution_file
|
|
737
|
+
>>> solution_file = csv_solution_file(
|
|
738
|
+
... name="solution",
|
|
739
|
+
... data=[{"id": 1, "value": 100}, {"id": 2, "value": 200}]
|
|
740
|
+
... )
|
|
741
|
+
>>> solution_file.name
|
|
742
|
+
'solution.csv'
|
|
743
|
+
>>> solution_file.data
|
|
744
|
+
[{'id': 1, 'value': 100}, {'id': 2, 'value': 200}]
|
|
745
|
+
"""
|
|
746
|
+
|
|
747
|
+
if not name.endswith(".csv"):
|
|
748
|
+
name += ".csv"
|
|
749
|
+
|
|
750
|
+
csv_configurations = csv_configurations or {}
|
|
751
|
+
|
|
752
|
+
def writer(file_path: str, write_data: list[dict[str, Any]]) -> None:
|
|
753
|
+
with open(file_path, "w", encoding="utf-8", newline="") as file:
|
|
754
|
+
writer = csv.DictWriter(
|
|
755
|
+
file,
|
|
756
|
+
fieldnames=write_data[0].keys(),
|
|
757
|
+
**csv_configurations,
|
|
758
|
+
)
|
|
759
|
+
writer.writeheader()
|
|
760
|
+
writer.writerows(write_data)
|
|
761
|
+
|
|
762
|
+
return SolutionFile(
|
|
763
|
+
name=name,
|
|
764
|
+
data=data,
|
|
765
|
+
writer=writer,
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
def text_solution_file(name: str, data: str) -> SolutionFile:
|
|
770
|
+
"""
|
|
771
|
+
This is a convenience function to build a `SolutionFile`. It writes the
|
|
772
|
+
given `data` to a utf-8 encoded file with the provided `name`.
|
|
773
|
+
|
|
774
|
+
You can import this function directly from `nextmv`:
|
|
775
|
+
|
|
776
|
+
```python
|
|
777
|
+
from nextmv import text_solution_file
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
You must provide the extension as part of the `name` parameter.
|
|
781
|
+
|
|
782
|
+
Parameters
|
|
783
|
+
----------
|
|
784
|
+
name : str
|
|
785
|
+
Name of the output file. The file extension must be provided in the
|
|
786
|
+
name.
|
|
787
|
+
data : str
|
|
788
|
+
The actual data that will be written to the file.
|
|
789
|
+
|
|
790
|
+
Returns
|
|
791
|
+
-------
|
|
792
|
+
SolutionFile
|
|
793
|
+
The constructed `SolutionFile` object.
|
|
794
|
+
|
|
795
|
+
Examples
|
|
796
|
+
--------
|
|
797
|
+
>>> from nextmv import text_solution_file
|
|
798
|
+
>>> solution_file = text_solution_file(
|
|
799
|
+
... name="solution.txt",
|
|
800
|
+
... data="This is a sample text solution."
|
|
801
|
+
... )
|
|
802
|
+
>>> solution_file.name
|
|
803
|
+
'solution.txt'
|
|
804
|
+
>>> solution_file.data
|
|
805
|
+
'This is a sample text solution.'
|
|
806
|
+
"""
|
|
807
|
+
|
|
808
|
+
def writer(file_path: str, write_data: str) -> None:
|
|
809
|
+
with open(file_path, "w", encoding="utf-8") as file:
|
|
810
|
+
file.write(write_data + "\n")
|
|
811
|
+
|
|
812
|
+
return SolutionFile(
|
|
813
|
+
name=name,
|
|
814
|
+
data=data,
|
|
815
|
+
writer=writer,
|
|
816
|
+
)
|
|
165
817
|
|
|
166
818
|
|
|
167
819
|
@dataclass
|
|
168
820
|
class Output:
|
|
169
821
|
"""
|
|
170
|
-
Output of a decision problem.
|
|
171
|
-
|
|
822
|
+
Output of a decision problem.
|
|
823
|
+
|
|
824
|
+
You can import the `Output` class directly from `nextmv`:
|
|
825
|
+
|
|
826
|
+
```python
|
|
827
|
+
from nextmv import Output
|
|
828
|
+
```
|
|
172
829
|
|
|
173
|
-
|
|
174
|
-
|
|
830
|
+
This class is used to structure the output of a decision problem that
|
|
831
|
+
can later be written to various destinations. It supports different output
|
|
832
|
+
formats and allows for customization of the serialization process.
|
|
175
833
|
|
|
176
|
-
|
|
177
|
-
serialized to the write location.
|
|
834
|
+
The `solution`'s type must match the `output_format`:
|
|
178
835
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
836
|
+
- `OutputFormat.JSON`: the data must be `dict[str, Any]` or `Any`.
|
|
837
|
+
- `OutputFormat.CSV_ARCHIVE`: the data must be `dict[str, list[dict[str,
|
|
838
|
+
Any]]]`. The keys represent the file names where the data should be
|
|
839
|
+
written. The values are lists of dictionaries, where each dictionary
|
|
840
|
+
represents a row in the CSV file.
|
|
182
841
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
842
|
+
If you are working with `OutputFormat.MULTI_FILE`, you should use
|
|
843
|
+
`solution_files` instead of `solution`. When `solution_files` is not
|
|
844
|
+
`None`, then the `output_format` _must_ be `OutputFormat.MULTI_FILE`.
|
|
845
|
+
`solution_files` is a list of `SolutionFile` objects, which allows you to
|
|
846
|
+
define the name of the file, the data to be written, and the writer
|
|
847
|
+
function that will handle the serialization of the data. This is useful when
|
|
848
|
+
you need to write the solution to multiple files with different formats or
|
|
849
|
+
configurations.
|
|
188
850
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
data
|
|
851
|
+
There are convenience functions to create `SolutionFile` objects for
|
|
852
|
+
common use cases, such as:
|
|
853
|
+
|
|
854
|
+
- `json_solution_file`: for writing JSON data to a file.
|
|
855
|
+
- `csv_solution_file`: for writing CSV data to a file.
|
|
856
|
+
- `text_solution_file`: for writing utf-8 encoded data to a file.
|
|
857
|
+
|
|
858
|
+
For other data types, such as Excel, you can create your own `SolutionFile`
|
|
859
|
+
objects by providing a `name`, `data`, and a `writer` function that will
|
|
860
|
+
handle the serialization of the data.
|
|
193
861
|
|
|
194
862
|
Parameters
|
|
195
863
|
----------
|
|
196
|
-
options : Options, optional
|
|
197
|
-
Options that the `
|
|
198
|
-
|
|
864
|
+
options : Optional[Union[Options, dict[str, Any]]], optional
|
|
865
|
+
Options that the `Output` was created with. These options can be of type
|
|
866
|
+
`Options` or a simple dictionary. Default is None.
|
|
867
|
+
output_format : Optional[OutputFormat], optional
|
|
199
868
|
Format of the output data. Default is `OutputFormat.JSON`.
|
|
200
|
-
solution : Union[
|
|
201
|
-
The solution to the decision problem.
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
869
|
+
solution : Optional[Union[dict[str, Any], Any, dict[str, list[dict[str, Any]]]]], optional
|
|
870
|
+
The solution to the decision problem. The type must match the
|
|
871
|
+
`output_format`. Default is None.
|
|
872
|
+
statistics : Optional[Union[Statistics, dict[str, Any]]], optional
|
|
873
|
+
Statistics of the solution. Default is None.
|
|
874
|
+
csv_configurations : Optional[dict[str, Any]], optional
|
|
875
|
+
Configuration for writing CSV files. Default is None.
|
|
876
|
+
json_configurations : Optional[dict[str, Any]], optional
|
|
877
|
+
Configuration for writing JSON files. Default is None.
|
|
878
|
+
assets : Optional[list[Union[Asset, dict[str, Any]]]], optional
|
|
879
|
+
List of assets to be included in the output. Default is None.
|
|
880
|
+
solution_files: Optional[list[SolutionFile]], default = None
|
|
881
|
+
Optional list of solution files to be included in the output. These
|
|
882
|
+
files are of type `SolutionFile`, which allows for custom serialization
|
|
883
|
+
and writing of the solution data to files. When this field is
|
|
884
|
+
specified, then the `output_format` must be set to
|
|
885
|
+
`OutputFormat.MULTI_FILE`, otherwise an exception will be raised. The
|
|
886
|
+
`SolutionFile` class allows you to define the name of the file, the
|
|
887
|
+
data to be written, and the writer function that will handle the
|
|
888
|
+
serialization of the data. This is useful when you need to write the
|
|
889
|
+
solution to multiple files with different formats or configurations.
|
|
890
|
+
|
|
891
|
+
There are convenience functions to create `SolutionFile` objects for
|
|
892
|
+
common use cases, such as:
|
|
893
|
+
|
|
894
|
+
- `json_solution_file`: for writing JSON data to a file.
|
|
895
|
+
- `csv_solution_file`: for writing CSV data to a file.
|
|
896
|
+
- `text_solution_file`: for writing utf-8 encoded data to a file.
|
|
897
|
+
|
|
898
|
+
For other data types, such as Excel, you can create your own
|
|
899
|
+
`SolutionFile` objects by providing a `name`, `data`, and a `writer`
|
|
900
|
+
function that will handle the serialization of the data.
|
|
901
|
+
|
|
902
|
+
Raises
|
|
903
|
+
------
|
|
904
|
+
ValueError
|
|
905
|
+
If the solution is not compatible with the specified output_format.
|
|
906
|
+
TypeError
|
|
907
|
+
If options, statistics, or assets have unsupported types.
|
|
908
|
+
|
|
909
|
+
Examples
|
|
910
|
+
--------
|
|
911
|
+
>>> from nextmv.output import Output, OutputFormat, Statistics, RunStatistics
|
|
912
|
+
>>> run_stats = RunStatistics(duration=30.0, iterations=100)
|
|
913
|
+
>>> stats = Statistics(run=run_stats)
|
|
914
|
+
>>> solution = {"routes": [{"vehicle": 1, "stops": [1, 2, 3]}, {"vehicle": 2, "stops": [4, 5]}]}
|
|
915
|
+
>>> output = Output(
|
|
916
|
+
... output_format=OutputFormat.JSON,
|
|
917
|
+
... solution=solution,
|
|
918
|
+
... statistics=stats,
|
|
919
|
+
... json_configurations={"indent": 4}
|
|
920
|
+
... )
|
|
921
|
+
>>> output_dict = output.to_dict()
|
|
922
|
+
>>> "solution" in output_dict and "statistics" in output_dict
|
|
923
|
+
True
|
|
924
|
+
"""
|
|
925
|
+
|
|
926
|
+
options: Options | dict[str, Any] | None = None
|
|
927
|
+
"""
|
|
928
|
+
Options that the `Output` was created with. These options can be of type
|
|
929
|
+
`Options` or a simple dictionary. If the options are of type `Options`,
|
|
930
|
+
they will be serialized to a dictionary using the `to_dict` method. If
|
|
931
|
+
they are a dictionary, they will be used as is. If the options are not
|
|
932
|
+
provided, an empty dictionary will be used. If the options are of type
|
|
933
|
+
`dict`, then the dictionary should have the following structure:
|
|
934
|
+
|
|
935
|
+
```python
|
|
936
|
+
{
|
|
937
|
+
"duration": "30",
|
|
938
|
+
"threads": 4,
|
|
939
|
+
}
|
|
940
|
+
```
|
|
941
|
+
"""
|
|
942
|
+
output_format: OutputFormat | None = OutputFormat.JSON
|
|
943
|
+
"""
|
|
944
|
+
Format of the output data. Default is `OutputFormat.JSON`. When set to
|
|
945
|
+
`OutputFormat.MULTI_FILE`, the `solution_files` field must be specified and
|
|
946
|
+
cannot be `None`.
|
|
947
|
+
"""
|
|
948
|
+
solution: dict[str, Any] | Any | dict[str, list[dict[str, Any]]] | None = None
|
|
949
|
+
"""
|
|
950
|
+
The solution to the decision problem. Use this filed when working with
|
|
951
|
+
`output_format` of types:
|
|
952
|
+
|
|
953
|
+
- `OutputFormat.JSON`: the data must be `dict[str, Any]` or `Any`.
|
|
954
|
+
- `OutputFormat.CSV_ARCHIVE`: the data must be `dict[str, list[dict[str,
|
|
955
|
+
Any]]]`. The keys represent the file names where the data will be written
|
|
956
|
+
to. The values are lists of dictionaries, where each dictionary represents
|
|
957
|
+
a row in the CSV file.
|
|
958
|
+
|
|
959
|
+
Note that when the `output_format` is set to `OutputFormat.MULTI_FILE`,
|
|
960
|
+
this `solution` field is ignored, as you should use the `solution_files`
|
|
961
|
+
field instead.
|
|
962
|
+
"""
|
|
963
|
+
statistics: Statistics | dict[str, Any] | None = None
|
|
964
|
+
"""
|
|
965
|
+
Statistics of the solution. These statistics can be of type `Statistics` or a
|
|
966
|
+
simple dictionary. If the statistics are of type `Statistics`, they will be
|
|
967
|
+
serialized to a dictionary using the `to_dict` method. If they are a
|
|
968
|
+
dictionary, they will be used as is. If the statistics are not provided, an
|
|
969
|
+
empty dictionary will be used.
|
|
970
|
+
"""
|
|
971
|
+
csv_configurations: dict[str, Any] | None = None
|
|
972
|
+
"""
|
|
973
|
+
Optional configuration for writing CSV files, to be used when the
|
|
974
|
+
`output_format` is `OutputFormat.CSV_ARCHIVE`. These configurations are
|
|
975
|
+
passed as kwargs to the `DictWriter` class from the `csv` module.
|
|
976
|
+
"""
|
|
977
|
+
json_configurations: dict[str, Any] | None = None
|
|
978
|
+
"""
|
|
979
|
+
Optional configuration for writing JSON files, to be used when the
|
|
980
|
+
`output_format` is `OutputFormat.JSON`. These configurations are passed as
|
|
981
|
+
kwargs to the `json.dumps` function.
|
|
982
|
+
"""
|
|
983
|
+
assets: list[Asset | dict[str, Any]] | None = None
|
|
984
|
+
"""
|
|
985
|
+
Optional list of assets to be included in the output. These assets can be of
|
|
986
|
+
type `Asset` or a simple dictionary. If the assets are of type `Asset`, they
|
|
987
|
+
will be serialized to a dictionary using the `to_dict` method. If they are a
|
|
988
|
+
dictionary, they will be used as is. If the assets are not provided, an
|
|
989
|
+
empty list will be used.
|
|
990
|
+
"""
|
|
991
|
+
solution_files: list[SolutionFile] | None = None
|
|
992
|
+
"""
|
|
993
|
+
Optional list of solution files to be included in the output. These files
|
|
994
|
+
are of type `SolutionFile`, which allows for custom serialization and
|
|
995
|
+
writing of the solution data to files. When this field is specified, then
|
|
996
|
+
the `output_format` must be set to `OutputFormat.MULTI_FILE`, otherwise an
|
|
997
|
+
exception will be raised. The `SolutionFile` class allows you to define the
|
|
998
|
+
name of the file, the data to be written, and the writer function that will
|
|
999
|
+
handle the serialization of the data. This is useful when you need to write
|
|
1000
|
+
the solution to multiple files with different formats or configurations.
|
|
1001
|
+
|
|
1002
|
+
There are convenience functions to create `SolutionFile` objects for
|
|
1003
|
+
common use cases, such as:
|
|
1004
|
+
|
|
1005
|
+
- `json_solution_file`: for writing JSON data to a file.
|
|
1006
|
+
- `csv_solution_file`: for writing CSV data to a file.
|
|
1007
|
+
- `text_solution_file`: for writing utf-8 encoded data to a file.
|
|
1008
|
+
|
|
1009
|
+
For other data types, such as Excel, you can create your own `SolutionFile`
|
|
1010
|
+
objects by providing a `name`, `data`, and a `writer` function that will
|
|
1011
|
+
handle the serialization of the data.
|
|
1012
|
+
"""
|
|
219
1013
|
|
|
220
1014
|
def __post_init__(self):
|
|
221
|
-
"""
|
|
222
|
-
|
|
1015
|
+
"""
|
|
1016
|
+
Initialize and validate the Output instance.
|
|
223
1017
|
|
|
1018
|
+
This method performs two main tasks:
|
|
1019
|
+
1. Creates a deep copy of the options to preserve the original values
|
|
1020
|
+
2. Validates that the solution matches the specified output_format
|
|
1021
|
+
|
|
1022
|
+
Raises
|
|
1023
|
+
------
|
|
1024
|
+
ValueError
|
|
1025
|
+
If the solution is not compatible with the specified output_format.
|
|
1026
|
+
"""
|
|
224
1027
|
# Capture a snapshot of the options that were used to create the class
|
|
225
1028
|
# so even if they are changed later, we have a record of the original.
|
|
226
1029
|
init_options = self.options
|
|
227
1030
|
new_options = copy.deepcopy(init_options)
|
|
228
1031
|
self.options = new_options
|
|
229
1032
|
|
|
230
|
-
if self.solution is None:
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
1033
|
+
if self.solution is not None:
|
|
1034
|
+
if self.output_format == OutputFormat.JSON:
|
|
1035
|
+
try:
|
|
1036
|
+
_ = serialize_json(self.solution)
|
|
1037
|
+
except (TypeError, OverflowError) as e:
|
|
1038
|
+
raise ValueError(
|
|
1039
|
+
f"Output has output_format OutputFormat.JSON and "
|
|
1040
|
+
f"Output.solution is of type {type(self.solution)}, which is not JSON serializable"
|
|
1041
|
+
) from e
|
|
1042
|
+
|
|
1043
|
+
elif self.output_format == OutputFormat.CSV_ARCHIVE and not isinstance(self.solution, dict):
|
|
237
1044
|
raise ValueError(
|
|
238
|
-
f"Output
|
|
239
|
-
|
|
240
|
-
)
|
|
1045
|
+
f"unsupported Output.solution type: {type(self.solution)} with "
|
|
1046
|
+
"output_format OutputFormat.CSV_ARCHIVE, supported type is `dict`"
|
|
1047
|
+
)
|
|
241
1048
|
|
|
242
|
-
|
|
1049
|
+
if self.solution_files is not None and self.output_format != OutputFormat.MULTI_FILE:
|
|
243
1050
|
raise ValueError(
|
|
244
|
-
f"
|
|
245
|
-
"output_format
|
|
1051
|
+
f"`solution_files` are not `None`, but `output_format` is different from `OutputFormat.MULTI_FILE`: "
|
|
1052
|
+
f"{self.output_format}. If you want to use `solution_files`, set `output_format` "
|
|
1053
|
+
"to `OutputFormat.MULTI_FILE`."
|
|
1054
|
+
)
|
|
1055
|
+
elif self.solution_files is not None and not isinstance(self.solution_files, list):
|
|
1056
|
+
raise TypeError(
|
|
1057
|
+
f"unsupported Output.solution_files type: {type(self.solution_files)}, supported type is `list`"
|
|
1058
|
+
)
|
|
1059
|
+
|
|
1060
|
+
def to_dict(self) -> dict[str, Any]: # noqa: C901
|
|
1061
|
+
"""
|
|
1062
|
+
Convert the `Output` object to a dictionary.
|
|
1063
|
+
|
|
1064
|
+
Returns
|
|
1065
|
+
-------
|
|
1066
|
+
dict[str, Any]
|
|
1067
|
+
The dictionary representation of the `Output` object.
|
|
1068
|
+
"""
|
|
1069
|
+
|
|
1070
|
+
# Options need to end up as a dict, so we achieve that based on the
|
|
1071
|
+
# type of options that were used to create the class.
|
|
1072
|
+
if self.options is None:
|
|
1073
|
+
options = {}
|
|
1074
|
+
elif isinstance(self.options, Options):
|
|
1075
|
+
options = self.options.to_dict()
|
|
1076
|
+
elif isinstance(self.options, dict):
|
|
1077
|
+
options = self.options
|
|
1078
|
+
else:
|
|
1079
|
+
raise TypeError(f"unsupported options type: {type(self.options)}, supported types are `Options` or `dict`")
|
|
1080
|
+
|
|
1081
|
+
# Statistics need to end up as a dict, so we achieve that based on the
|
|
1082
|
+
# type of statistics that were used to create the class.
|
|
1083
|
+
if self.statistics is None:
|
|
1084
|
+
statistics = {}
|
|
1085
|
+
elif isinstance(self.statistics, Statistics):
|
|
1086
|
+
statistics = self.statistics.to_dict()
|
|
1087
|
+
elif isinstance(self.statistics, dict):
|
|
1088
|
+
statistics = self.statistics
|
|
1089
|
+
else:
|
|
1090
|
+
raise TypeError(
|
|
1091
|
+
f"unsupported statistics type: {type(self.statistics)}, supported types are `Statistics` or `dict`"
|
|
246
1092
|
)
|
|
247
1093
|
|
|
1094
|
+
# Assets need to end up as a list of dicts, so we achieve that based on
|
|
1095
|
+
# the type of each asset in the list.
|
|
1096
|
+
assets = []
|
|
1097
|
+
if isinstance(self.assets, list):
|
|
1098
|
+
for ix, asset in enumerate(self.assets):
|
|
1099
|
+
if isinstance(asset, Asset):
|
|
1100
|
+
assets.append(asset.to_dict())
|
|
1101
|
+
elif isinstance(asset, dict):
|
|
1102
|
+
assets.append(asset)
|
|
1103
|
+
else:
|
|
1104
|
+
raise TypeError(
|
|
1105
|
+
f"unsupported asset {ix}, type: {type(asset)}; supported types are `Asset` or `dict`"
|
|
1106
|
+
)
|
|
1107
|
+
elif self.assets is not None:
|
|
1108
|
+
raise TypeError(f"unsupported assets type: {type(self.assets)}, supported types are `list`")
|
|
1109
|
+
|
|
1110
|
+
output_dict = {
|
|
1111
|
+
"options": options,
|
|
1112
|
+
"solution": self.solution if self.solution is not None else {},
|
|
1113
|
+
STATISTICS_KEY: statistics,
|
|
1114
|
+
ASSETS_KEY: assets,
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
# Add the auxiliary configurations to the output dictionary if they are
|
|
1118
|
+
# defined and not empty.
|
|
1119
|
+
if (
|
|
1120
|
+
self.output_format == OutputFormat.CSV_ARCHIVE
|
|
1121
|
+
and self.csv_configurations is not None
|
|
1122
|
+
and self.csv_configurations != {}
|
|
1123
|
+
):
|
|
1124
|
+
output_dict["csv_configurations"] = self.csv_configurations
|
|
1125
|
+
|
|
1126
|
+
return output_dict
|
|
1127
|
+
|
|
248
1128
|
|
|
249
1129
|
class OutputWriter:
|
|
250
|
-
"""
|
|
1130
|
+
"""
|
|
1131
|
+
Base class for writing outputs.
|
|
251
1132
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
1133
|
+
You can import the `OutputWriter` class directly from `nextmv`:
|
|
1134
|
+
|
|
1135
|
+
```python
|
|
1136
|
+
from nextmv import OutputWriter
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
This is an abstract base class that defines the interface for writing outputs
|
|
1140
|
+
to different destinations. Subclasses should implement the `write` method.
|
|
1141
|
+
|
|
1142
|
+
Examples
|
|
1143
|
+
--------
|
|
1144
|
+
>>> class CustomOutputWriter(OutputWriter):
|
|
1145
|
+
... def write(self, output, path=None, **kwargs):
|
|
1146
|
+
... # Custom implementation for writing output
|
|
1147
|
+
... print(f"Writing output to {path}")
|
|
1148
|
+
"""
|
|
1149
|
+
|
|
1150
|
+
def write(self, output: Output | dict[str, Any] | BaseModel, *args, **kwargs) -> None:
|
|
255
1151
|
"""
|
|
1152
|
+
Write the output data.
|
|
256
1153
|
|
|
1154
|
+
This is an abstract method that should be implemented by subclasses.
|
|
1155
|
+
|
|
1156
|
+
Parameters
|
|
1157
|
+
----------
|
|
1158
|
+
output : Union[Output, dict[str, Any], BaseModel]
|
|
1159
|
+
The output data to write.
|
|
1160
|
+
*args
|
|
1161
|
+
Variable length argument list.
|
|
1162
|
+
**kwargs
|
|
1163
|
+
Arbitrary keyword arguments.
|
|
1164
|
+
|
|
1165
|
+
Raises
|
|
1166
|
+
------
|
|
1167
|
+
NotImplementedError
|
|
1168
|
+
This method must be implemented by subclasses.
|
|
1169
|
+
"""
|
|
257
1170
|
raise NotImplementedError
|
|
258
1171
|
|
|
259
1172
|
|
|
260
1173
|
class LocalOutputWriter(OutputWriter):
|
|
261
1174
|
"""
|
|
262
|
-
Class for
|
|
263
|
-
|
|
1175
|
+
Class for writing outputs to local files or stdout.
|
|
1176
|
+
|
|
1177
|
+
You can import the `LocalOutputWriter` class directly from `nextmv`:
|
|
1178
|
+
|
|
1179
|
+
```python
|
|
1180
|
+
from nextmv import LocalOutputWriter
|
|
1181
|
+
```
|
|
1182
|
+
|
|
1183
|
+
This class implements the OutputWriter interface to write output data to
|
|
1184
|
+
local files or stdout. The destination and format depend on the output
|
|
1185
|
+
format and the provided path.
|
|
1186
|
+
|
|
1187
|
+
Examples
|
|
1188
|
+
--------
|
|
1189
|
+
>>> from nextmv.output import LocalOutputWriter, Output, Statistics
|
|
1190
|
+
>>> writer = LocalOutputWriter()
|
|
1191
|
+
>>> output = Output(solution={"result": 42}, statistics=Statistics())
|
|
1192
|
+
>>> # Write to stdout
|
|
1193
|
+
>>> writer.write(output, path=None)
|
|
1194
|
+
>>> # Write to a file
|
|
1195
|
+
>>> writer.write(output, path="results.json")
|
|
264
1196
|
"""
|
|
265
1197
|
|
|
266
1198
|
def _write_json(
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
path:
|
|
1199
|
+
self,
|
|
1200
|
+
output: Output | dict[str, Any] | BaseModel,
|
|
1201
|
+
output_dict: dict[str, Any],
|
|
1202
|
+
path: str | None = None,
|
|
271
1203
|
) -> None:
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
sol = output.solution
|
|
275
|
-
elif isinstance(output, Dict):
|
|
276
|
-
sol = output.get("solution")
|
|
1204
|
+
"""
|
|
1205
|
+
Write output in JSON format.
|
|
277
1206
|
|
|
278
|
-
|
|
279
|
-
|
|
1207
|
+
Parameters
|
|
1208
|
+
----------
|
|
1209
|
+
output : Union[Output, dict[str, Any], BaseModel]
|
|
1210
|
+
The output object containing configuration.
|
|
1211
|
+
output_dict : dict[str, Any]
|
|
1212
|
+
Dictionary representation of the output to write.
|
|
1213
|
+
path : str, optional
|
|
1214
|
+
Path to write the output. If None or empty, writes to stdout.
|
|
1215
|
+
"""
|
|
1216
|
+
json_configurations = {}
|
|
1217
|
+
if hasattr(output, "json_configurations") and output.json_configurations is not None:
|
|
1218
|
+
json_configurations = output.json_configurations
|
|
280
1219
|
|
|
281
|
-
serialized =
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
"solution": solution,
|
|
285
|
-
"statistics": statistics,
|
|
286
|
-
},
|
|
287
|
-
indent=2,
|
|
288
|
-
default=_custom_serial,
|
|
1220
|
+
serialized = serialize_json(
|
|
1221
|
+
output_dict,
|
|
1222
|
+
json_configurations=json_configurations,
|
|
289
1223
|
)
|
|
290
1224
|
|
|
291
1225
|
if path is None or path == "":
|
|
@@ -296,11 +1230,29 @@ class LocalOutputWriter(OutputWriter):
|
|
|
296
1230
|
file.write(serialized + "\n")
|
|
297
1231
|
|
|
298
1232
|
def _write_archive(
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
path:
|
|
1233
|
+
self,
|
|
1234
|
+
output: Output | dict[str, Any] | BaseModel,
|
|
1235
|
+
output_dict: dict[str, Any],
|
|
1236
|
+
path: str | None = None,
|
|
303
1237
|
) -> None:
|
|
1238
|
+
"""
|
|
1239
|
+
Write output in CSV archive format.
|
|
1240
|
+
|
|
1241
|
+
Parameters
|
|
1242
|
+
----------
|
|
1243
|
+
output : Union[Output, dict[str, Any], BaseModel]
|
|
1244
|
+
The output object containing configuration and solution data.
|
|
1245
|
+
output_dict : dict[str, Any]
|
|
1246
|
+
Dictionary representation of the output to write.
|
|
1247
|
+
path : str, optional
|
|
1248
|
+
Directory path to write the CSV files. If None or empty,
|
|
1249
|
+
writes to a directory named "output" in the current working directory.
|
|
1250
|
+
|
|
1251
|
+
Raises
|
|
1252
|
+
------
|
|
1253
|
+
ValueError
|
|
1254
|
+
If the path is an existing file instead of a directory.
|
|
1255
|
+
"""
|
|
304
1256
|
dir_path = "output"
|
|
305
1257
|
if path is not None and path != "":
|
|
306
1258
|
if os.path.isfile(path):
|
|
@@ -311,71 +1263,215 @@ class LocalOutputWriter(OutputWriter):
|
|
|
311
1263
|
if not os.path.exists(dir_path):
|
|
312
1264
|
os.makedirs(dir_path)
|
|
313
1265
|
|
|
314
|
-
|
|
1266
|
+
json_configurations = {}
|
|
1267
|
+
if hasattr(output, "json_configurations") and output.json_configurations is not None:
|
|
1268
|
+
json_configurations = output.json_configurations
|
|
1269
|
+
|
|
1270
|
+
serialized = serialize_json(
|
|
315
1271
|
{
|
|
316
|
-
"options": options,
|
|
317
|
-
|
|
1272
|
+
"options": output_dict.get("options", {}),
|
|
1273
|
+
STATISTICS_KEY: output_dict.get(STATISTICS_KEY, {}),
|
|
1274
|
+
ASSETS_KEY: output_dict.get(ASSETS_KEY, []),
|
|
318
1275
|
},
|
|
319
|
-
|
|
1276
|
+
json_configurations=json_configurations,
|
|
320
1277
|
)
|
|
321
1278
|
print(serialized, file=sys.stdout)
|
|
322
1279
|
|
|
323
1280
|
if output.solution is None:
|
|
324
1281
|
return
|
|
325
1282
|
|
|
1283
|
+
csv_configurations = output.csv_configurations
|
|
1284
|
+
if csv_configurations is None:
|
|
1285
|
+
csv_configurations = {}
|
|
1286
|
+
|
|
326
1287
|
for file_name, data in output.solution.items():
|
|
327
1288
|
file_path = os.path.join(dir_path, f"{file_name}.csv")
|
|
328
1289
|
with open(file_path, "w", encoding="utf-8", newline="") as file:
|
|
329
1290
|
writer = csv.DictWriter(
|
|
330
1291
|
file,
|
|
331
1292
|
fieldnames=data[0].keys(),
|
|
332
|
-
|
|
1293
|
+
**csv_configurations,
|
|
333
1294
|
)
|
|
334
1295
|
writer.writeheader()
|
|
335
1296
|
writer.writerows(data)
|
|
336
1297
|
|
|
1298
|
+
def _write_multi_file(
|
|
1299
|
+
self,
|
|
1300
|
+
output: Output | dict[str, Any] | BaseModel,
|
|
1301
|
+
output_dict: dict[str, Any],
|
|
1302
|
+
path: str | None = None,
|
|
1303
|
+
) -> None:
|
|
1304
|
+
"""
|
|
1305
|
+
Write output to multiple files.
|
|
1306
|
+
|
|
1307
|
+
Parameters
|
|
1308
|
+
----------
|
|
1309
|
+
output : Union[Output, dict[str, Any], BaseModel]
|
|
1310
|
+
The output object containing configuration and solution data.
|
|
1311
|
+
output_dict : dict[str, Any]
|
|
1312
|
+
Dictionary representation of the output to write.
|
|
1313
|
+
path : str, optional
|
|
1314
|
+
Directory path to write the CSV files. If None or empty,
|
|
1315
|
+
writes to a directory named "output" in the current working directory.
|
|
1316
|
+
|
|
1317
|
+
Raises
|
|
1318
|
+
------
|
|
1319
|
+
ValueError
|
|
1320
|
+
If the path is an existing file instead of a directory.
|
|
1321
|
+
"""
|
|
1322
|
+
dir_path = OUTPUTS_KEY
|
|
1323
|
+
if path is not None and path != "":
|
|
1324
|
+
if os.path.isfile(path):
|
|
1325
|
+
raise ValueError(f"The path refers to an existing file: {path}")
|
|
1326
|
+
|
|
1327
|
+
dir_path = path
|
|
1328
|
+
|
|
1329
|
+
if not os.path.exists(dir_path):
|
|
1330
|
+
os.makedirs(dir_path)
|
|
1331
|
+
|
|
1332
|
+
json_configurations = {}
|
|
1333
|
+
if hasattr(output, "json_configurations") and output.json_configurations is not None:
|
|
1334
|
+
json_configurations = output.json_configurations
|
|
1335
|
+
|
|
1336
|
+
self._write_multi_file_element(
|
|
1337
|
+
parent_dir=dir_path,
|
|
1338
|
+
json_configurations=json_configurations,
|
|
1339
|
+
output_dict=output_dict,
|
|
1340
|
+
element_key=STATISTICS_KEY,
|
|
1341
|
+
)
|
|
1342
|
+
self._write_multi_file_element(
|
|
1343
|
+
parent_dir=dir_path,
|
|
1344
|
+
json_configurations=json_configurations,
|
|
1345
|
+
output_dict=output_dict,
|
|
1346
|
+
element_key=ASSETS_KEY,
|
|
1347
|
+
)
|
|
1348
|
+
self._write_multi_file_solution(dir_path=dir_path, output=output)
|
|
1349
|
+
|
|
1350
|
+
def _write_multi_file_element(
|
|
1351
|
+
self,
|
|
1352
|
+
parent_dir: str,
|
|
1353
|
+
output_dict: dict[str, Any],
|
|
1354
|
+
element_key: str,
|
|
1355
|
+
json_configurations: dict[str, Any] | None = None,
|
|
1356
|
+
):
|
|
1357
|
+
"""
|
|
1358
|
+
Auxiliary function to write a specific element of the output
|
|
1359
|
+
dictionary to a file in the specified parent directory.
|
|
1360
|
+
"""
|
|
1361
|
+
|
|
1362
|
+
element = output_dict.get(element_key)
|
|
1363
|
+
if element is None or not element:
|
|
1364
|
+
return
|
|
1365
|
+
|
|
1366
|
+
final_dir = os.path.join(parent_dir, element_key)
|
|
1367
|
+
|
|
1368
|
+
if not os.path.exists(final_dir):
|
|
1369
|
+
os.makedirs(final_dir)
|
|
1370
|
+
|
|
1371
|
+
keyed_element = {element_key: element} # The element is expected behind its key.
|
|
1372
|
+
|
|
1373
|
+
serialized = serialize_json(keyed_element, json_configurations=json_configurations)
|
|
1374
|
+
|
|
1375
|
+
with open(os.path.join(final_dir, f"{element_key}.json"), "w", encoding="utf-8") as file:
|
|
1376
|
+
file.write(serialized + "\n")
|
|
1377
|
+
|
|
1378
|
+
def _write_multi_file_solution(
|
|
1379
|
+
self,
|
|
1380
|
+
dir_path: str,
|
|
1381
|
+
output: Output,
|
|
1382
|
+
):
|
|
1383
|
+
"""
|
|
1384
|
+
Auxiliary function to write the solution files to the specified
|
|
1385
|
+
directory.
|
|
1386
|
+
"""
|
|
1387
|
+
|
|
1388
|
+
if output.solution_files is None:
|
|
1389
|
+
return
|
|
1390
|
+
|
|
1391
|
+
solutions_dir = os.path.join(dir_path, SOLUTIONS_KEY)
|
|
1392
|
+
|
|
1393
|
+
if not os.path.exists(solutions_dir):
|
|
1394
|
+
os.makedirs(solutions_dir)
|
|
1395
|
+
|
|
1396
|
+
for solution_file in output.solution_files:
|
|
1397
|
+
if not isinstance(solution_file, SolutionFile):
|
|
1398
|
+
raise TypeError(
|
|
1399
|
+
f"unsupported solution_file type: {type(solution_file)}, supported type is `SolutionFile`"
|
|
1400
|
+
)
|
|
1401
|
+
|
|
1402
|
+
file_path = os.path.join(solutions_dir, solution_file.name)
|
|
1403
|
+
if solution_file.writer_args is None:
|
|
1404
|
+
solution_file.writer_args = []
|
|
1405
|
+
if solution_file.writer_kwargs is None:
|
|
1406
|
+
solution_file.writer_kwargs = {}
|
|
1407
|
+
|
|
1408
|
+
# Call the writer function with the final path, and user provided
|
|
1409
|
+
# arguments and keyword arguments.
|
|
1410
|
+
solution_file.writer(
|
|
1411
|
+
file_path,
|
|
1412
|
+
solution_file.data,
|
|
1413
|
+
*solution_file.writer_args,
|
|
1414
|
+
**solution_file.writer_kwargs,
|
|
1415
|
+
)
|
|
1416
|
+
|
|
337
1417
|
# Callback functions for writing the output data.
|
|
338
1418
|
FILE_WRITERS = {
|
|
339
1419
|
OutputFormat.JSON: _write_json,
|
|
340
1420
|
OutputFormat.CSV_ARCHIVE: _write_archive,
|
|
1421
|
+
OutputFormat.MULTI_FILE: _write_multi_file,
|
|
1422
|
+
OutputFormat.TEXT: _write_json,
|
|
341
1423
|
}
|
|
1424
|
+
"""Dictionary mapping output formats to writer functions."""
|
|
342
1425
|
|
|
343
1426
|
def write(
|
|
344
1427
|
self,
|
|
345
|
-
output:
|
|
346
|
-
path:
|
|
1428
|
+
output: Output | dict[str, Any] | BaseModel,
|
|
1429
|
+
path: str | None = None,
|
|
347
1430
|
skip_stdout_reset: bool = False,
|
|
348
1431
|
) -> None:
|
|
349
1432
|
"""
|
|
350
|
-
Write the
|
|
351
|
-
the `path` parameter, depending on the `Output.output_format`:
|
|
352
|
-
|
|
353
|
-
- `OutputFormat.JSON`: the `path` is the file where the JSON data will
|
|
354
|
-
be written. If empty or `None`, the data will be written to stdout.
|
|
355
|
-
- `OutputFormat.CSV_ARCHIVE`: the `path` is the directory where the CSV
|
|
356
|
-
files will be written. If empty or `None`, the data will be written
|
|
357
|
-
to a directory named `output` under the current working directory.
|
|
358
|
-
The `Output.options` and `Output.statistics` will be written to
|
|
359
|
-
stdout.
|
|
1433
|
+
Write the output to the local filesystem or stdout.
|
|
360
1434
|
|
|
361
|
-
This
|
|
362
|
-
|
|
363
|
-
`skip_stdout_reset` parameter to `True`.
|
|
1435
|
+
This method writes the provided output to the specified path or to stdout,
|
|
1436
|
+
depending on the output format and the path parameter.
|
|
364
1437
|
|
|
365
1438
|
Parameters
|
|
366
1439
|
----------
|
|
367
|
-
output: Output,
|
|
368
|
-
Output data to write.
|
|
369
|
-
path : str
|
|
370
|
-
Path to write the output data to.
|
|
1440
|
+
output : Union[Output, dict[str, Any], BaseModel]
|
|
1441
|
+
Output data to write. Can be an Output object, a dictionary, or a BaseModel.
|
|
1442
|
+
path : str, optional
|
|
1443
|
+
Path to write the output data to. The interpretation depends on the output format:
|
|
1444
|
+
- For OutputFormat.JSON: File path for the JSON output. If None or empty, writes to stdout.
|
|
1445
|
+
- For OutputFormat.CSV_ARCHIVE: Directory path for CSV files. If None or empty,
|
|
1446
|
+
writes to a directory named "output" in the current working directory.
|
|
371
1447
|
skip_stdout_reset : bool, optional
|
|
372
|
-
Skip resetting stdout before writing the output data. Default is
|
|
373
|
-
`False`.
|
|
1448
|
+
Skip resetting stdout before writing the output data. Default is False.
|
|
374
1449
|
|
|
375
1450
|
Raises
|
|
376
1451
|
------
|
|
377
1452
|
ValueError
|
|
378
|
-
If the
|
|
1453
|
+
If the Output.output_format is not supported.
|
|
1454
|
+
TypeError
|
|
1455
|
+
If the output is of an unsupported type.
|
|
1456
|
+
|
|
1457
|
+
Notes
|
|
1458
|
+
-----
|
|
1459
|
+
This function detects if stdout was redirected and resets it to avoid
|
|
1460
|
+
unexpected behavior. If you want to skip this behavior, set the
|
|
1461
|
+
skip_stdout_reset parameter to True.
|
|
1462
|
+
|
|
1463
|
+
If the output is a dict or a BaseModel, it will be written as JSON. If
|
|
1464
|
+
the output is an Output object, it will be written according to its
|
|
1465
|
+
output_format.
|
|
1466
|
+
|
|
1467
|
+
Examples
|
|
1468
|
+
--------
|
|
1469
|
+
>>> from nextmv.output import LocalOutputWriter, Output
|
|
1470
|
+
>>> writer = LocalOutputWriter()
|
|
1471
|
+
>>> # Write JSON to a file
|
|
1472
|
+
>>> writer.write(Output(solution={"result": 42}), path="result.json")
|
|
1473
|
+
>>> # Write JSON to stdout
|
|
1474
|
+
>>> writer.write({"simple": "data"})
|
|
379
1475
|
"""
|
|
380
1476
|
|
|
381
1477
|
# If the user forgot to reset stdout after redirecting it, we need to
|
|
@@ -385,116 +1481,152 @@ class LocalOutputWriter(OutputWriter):
|
|
|
385
1481
|
|
|
386
1482
|
if isinstance(output, Output):
|
|
387
1483
|
output_format = output.output_format
|
|
388
|
-
elif isinstance(output,
|
|
1484
|
+
elif isinstance(output, dict):
|
|
1485
|
+
output_format = OutputFormat.JSON
|
|
1486
|
+
elif isinstance(output, BaseModel):
|
|
389
1487
|
output_format = OutputFormat.JSON
|
|
390
1488
|
else:
|
|
391
|
-
raise TypeError(
|
|
1489
|
+
raise TypeError(
|
|
1490
|
+
f"unsupported output type: {type(output)}, supported types are `Output`, `dict`, `BaseModel`"
|
|
1491
|
+
)
|
|
392
1492
|
|
|
393
|
-
|
|
394
|
-
|
|
1493
|
+
output_dict = {}
|
|
1494
|
+
if isinstance(output, Output):
|
|
1495
|
+
output_dict = output.to_dict()
|
|
1496
|
+
elif isinstance(output, BaseModel):
|
|
1497
|
+
output_dict = output.to_dict()
|
|
1498
|
+
elif isinstance(output, dict):
|
|
1499
|
+
output_dict = output
|
|
1500
|
+
else:
|
|
1501
|
+
raise TypeError(
|
|
1502
|
+
f"unsupported output type: {type(output)}, supported types are `Output`, `dict`, `BaseModel`"
|
|
1503
|
+
)
|
|
395
1504
|
|
|
396
1505
|
self.FILE_WRITERS[output_format](
|
|
1506
|
+
self,
|
|
397
1507
|
output=output,
|
|
398
|
-
|
|
399
|
-
statistics=statistics,
|
|
1508
|
+
output_dict=output_dict,
|
|
400
1509
|
path=path,
|
|
401
1510
|
)
|
|
402
1511
|
|
|
403
|
-
@staticmethod
|
|
404
|
-
def _extract_statistics(output: Union[Output, Dict[str, Any]]) -> Dict[str, Any]:
|
|
405
|
-
"""Extract JSON-serializable statistics."""
|
|
406
1512
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
1513
|
+
def write_local(
|
|
1514
|
+
output: Output | dict[str, Any],
|
|
1515
|
+
path: str | None = None,
|
|
1516
|
+
skip_stdout_reset: bool = False,
|
|
1517
|
+
) -> None:
|
|
1518
|
+
"""
|
|
1519
|
+
!!! warning
|
|
1520
|
+
`write_local` is deprecated, use `write` instead.
|
|
413
1521
|
|
|
414
|
-
|
|
415
|
-
return statistics
|
|
1522
|
+
Write the output to the local filesystem or stdout.
|
|
416
1523
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
elif isinstance(stats, Dict):
|
|
420
|
-
statistics = stats
|
|
421
|
-
else:
|
|
422
|
-
raise TypeError(f"unsupported statistics type: {type(stats)}, supported types are `Statistics` or `Dict`")
|
|
1524
|
+
This is a convenience function for instantiating a `LocalOutputWriter` and
|
|
1525
|
+
calling its `write` method.
|
|
423
1526
|
|
|
424
|
-
|
|
1527
|
+
Parameters
|
|
1528
|
+
----------
|
|
1529
|
+
output : Union[Output, dict[str, Any]]
|
|
1530
|
+
Output data to write. Can be an Output object or a dictionary.
|
|
1531
|
+
path : str, optional
|
|
1532
|
+
Path to write the output data to. The interpretation depends on the
|
|
1533
|
+
output format:
|
|
1534
|
+
|
|
1535
|
+
- For `OutputFormat.JSON`: File path for the JSON output. If None or
|
|
1536
|
+
empty, writes to stdout.
|
|
1537
|
+
- For `OutputFormat.CSV_ARCHIVE`: Directory path for CSV files. If None
|
|
1538
|
+
or empty, writes to a directory named "output" in the current working
|
|
1539
|
+
directory.
|
|
1540
|
+
skip_stdout_reset : bool, optional
|
|
1541
|
+
Skip resetting stdout before writing the output data. Default is False.
|
|
425
1542
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
1543
|
+
Raises
|
|
1544
|
+
------
|
|
1545
|
+
ValueError
|
|
1546
|
+
If the Output.output_format is not supported.
|
|
1547
|
+
TypeError
|
|
1548
|
+
If the output is of an unsupported type.
|
|
429
1549
|
|
|
430
|
-
|
|
1550
|
+
Notes
|
|
1551
|
+
-----
|
|
1552
|
+
This function detects if stdout was redirected and resets it to avoid
|
|
1553
|
+
unexpected behavior. If you want to skip this behavior, set the
|
|
1554
|
+
skip_stdout_reset parameter to True.
|
|
1555
|
+
|
|
1556
|
+
Examples
|
|
1557
|
+
--------
|
|
1558
|
+
>>> from nextmv.output import write_local, Output
|
|
1559
|
+
>>> # Write JSON to a file
|
|
1560
|
+
>>> write_local(Output(solution={"result": 42}), path="result.json")
|
|
1561
|
+
>>> # Write JSON to stdout
|
|
1562
|
+
>>> write_local({"simple": "data"})
|
|
1563
|
+
"""
|
|
431
1564
|
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
1565
|
+
deprecated(
|
|
1566
|
+
name="write_local",
|
|
1567
|
+
reason="`write_local` is deprecated, use `write` instead.",
|
|
1568
|
+
)
|
|
436
1569
|
|
|
437
|
-
|
|
438
|
-
|
|
1570
|
+
writer = LocalOutputWriter()
|
|
1571
|
+
writer.write(output, path, skip_stdout_reset)
|
|
439
1572
|
|
|
440
|
-
if isinstance(opt, Options):
|
|
441
|
-
options = opt.to_dict()
|
|
442
|
-
elif isinstance(opt, Dict):
|
|
443
|
-
options = opt
|
|
444
|
-
else:
|
|
445
|
-
raise TypeError(f"unsupported options type: {type(opt)}, supported types are `Options` or `Dict`")
|
|
446
1573
|
|
|
447
|
-
|
|
1574
|
+
_LOCAL_OUTPUT_WRITER = LocalOutputWriter()
|
|
1575
|
+
"""Default LocalOutputWriter instance used by the write function."""
|
|
448
1576
|
|
|
449
1577
|
|
|
450
|
-
def
|
|
451
|
-
output:
|
|
452
|
-
path:
|
|
1578
|
+
def write(
|
|
1579
|
+
output: Output | dict[str, Any] | BaseModel,
|
|
1580
|
+
path: str | None = None,
|
|
453
1581
|
skip_stdout_reset: bool = False,
|
|
1582
|
+
writer: OutputWriter | None = _LOCAL_OUTPUT_WRITER,
|
|
454
1583
|
) -> None:
|
|
455
1584
|
"""
|
|
456
|
-
|
|
457
|
-
calling its `write` method.
|
|
1585
|
+
Write the output to the specified destination.
|
|
458
1586
|
|
|
459
|
-
|
|
460
|
-
`path` parameter, depending on the `Output.output_format`:
|
|
1587
|
+
You can import the `write` function directly from `nextmv`:
|
|
461
1588
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
files will be written. If empty or `None`, the data will be written
|
|
466
|
-
to a directory named `output` under the current working directory.
|
|
467
|
-
The `Output.options` and `Output.statistics` will be written to
|
|
468
|
-
stdout.
|
|
1589
|
+
```python
|
|
1590
|
+
from nextmv import write
|
|
1591
|
+
```
|
|
469
1592
|
|
|
470
|
-
This
|
|
471
|
-
|
|
472
|
-
`skip_stdout_reset` parameter to `True`.
|
|
1593
|
+
This is a convenience function for writing output data using a provided writer.
|
|
1594
|
+
By default, it uses the `LocalOutputWriter` to write to files or stdout.
|
|
473
1595
|
|
|
474
1596
|
Parameters
|
|
475
1597
|
----------
|
|
476
|
-
output : Output,
|
|
477
|
-
Output data to write.
|
|
478
|
-
path : str
|
|
479
|
-
Path to write the output data to.
|
|
1598
|
+
output : Union[Output, dict[str, Any], BaseModel]
|
|
1599
|
+
Output data to write. Can be an Output object, a dictionary, or a BaseModel.
|
|
1600
|
+
path : str, optional
|
|
1601
|
+
Path to write the output data to. The interpretation depends on the
|
|
1602
|
+
output format:
|
|
1603
|
+
|
|
1604
|
+
- For `OutputFormat.JSON`: File path for the JSON output. If None or
|
|
1605
|
+
empty, writes to stdout.
|
|
1606
|
+
- For `OutputFormat.CSV_ARCHIVE`: Directory path for CSV files. If None
|
|
1607
|
+
or empty, writes to a directory named "output" in the current working
|
|
1608
|
+
directory.
|
|
480
1609
|
skip_stdout_reset : bool, optional
|
|
481
|
-
Skip resetting stdout before writing the output data. Default is
|
|
482
|
-
|
|
1610
|
+
Skip resetting stdout before writing the output data. Default is False.
|
|
1611
|
+
writer : OutputWriter, optional
|
|
1612
|
+
The writer to use for writing the output. Default is a
|
|
1613
|
+
`LocalOutputWriter` instance.
|
|
483
1614
|
|
|
484
1615
|
Raises
|
|
485
1616
|
------
|
|
486
1617
|
ValueError
|
|
487
|
-
If the
|
|
1618
|
+
If the Output.output_format is not supported.
|
|
1619
|
+
TypeError
|
|
1620
|
+
If the output is of an unsupported type.
|
|
1621
|
+
|
|
1622
|
+
Examples
|
|
1623
|
+
--------
|
|
1624
|
+
>>> from nextmv.output import write, Output, OutputFormat
|
|
1625
|
+
>>> # Write JSON to a file
|
|
1626
|
+
>>> write(Output(solution={"result": 42}), path="result.json")
|
|
1627
|
+
>>> # Write CSV archive
|
|
1628
|
+
>>> data = {"vehicles": [{"id": 1, "capacity": 100}, {"id": 2, "capacity": 150}]}
|
|
1629
|
+
>>> write(Output(output_format=OutputFormat.CSV_ARCHIVE, solution=data), path="output_dir")
|
|
488
1630
|
"""
|
|
489
1631
|
|
|
490
|
-
writer = LocalOutputWriter()
|
|
491
1632
|
writer.write(output, path, skip_stdout_reset)
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
def _custom_serial(obj: Any):
|
|
495
|
-
"""JSON serializer for objects not serializable by default one."""
|
|
496
|
-
|
|
497
|
-
if isinstance(obj, (datetime.datetime | datetime.date)):
|
|
498
|
-
return obj.isoformat()
|
|
499
|
-
|
|
500
|
-
raise TypeError(f"Type {type(obj)} not serializable")
|