nextmv 0.27.0__py3-none-any.whl → 0.28.1.dev0__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/__init__.py +1 -0
- nextmv/base_model.py +52 -7
- nextmv/cloud/__init__.py +3 -0
- nextmv/cloud/acceptance_test.py +711 -20
- nextmv/cloud/account.py +152 -7
- nextmv/cloud/application.py +1213 -382
- nextmv/cloud/batch_experiment.py +133 -21
- nextmv/cloud/client.py +240 -46
- nextmv/cloud/input_set.py +96 -3
- nextmv/cloud/instance.py +89 -3
- nextmv/cloud/manifest.py +508 -132
- nextmv/cloud/package.py +2 -2
- nextmv/cloud/run.py +372 -41
- nextmv/cloud/safe.py +7 -7
- nextmv/cloud/scenario.py +205 -20
- nextmv/cloud/secrets.py +179 -6
- nextmv/cloud/status.py +95 -2
- nextmv/cloud/version.py +132 -4
- nextmv/deprecated.py +36 -2
- nextmv/input.py +298 -80
- nextmv/logger.py +71 -7
- nextmv/model.py +223 -56
- nextmv/options.py +281 -66
- nextmv/output.py +552 -159
- {nextmv-0.27.0.dist-info → nextmv-0.28.1.dev0.dist-info}/METADATA +24 -4
- nextmv-0.28.1.dev0.dist-info/RECORD +30 -0
- nextmv-0.27.0.dist-info/RECORD +0 -30
- {nextmv-0.27.0.dist-info → nextmv-0.28.1.dev0.dist-info}/WHEEL +0 -0
- {nextmv-0.27.0.dist-info → nextmv-0.28.1.dev0.dist-info}/licenses/LICENSE +0 -0
nextmv/output.py
CHANGED
|
@@ -1,4 +1,44 @@
|
|
|
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
|
+
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
|
|
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
|
-
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
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
|
|
189
|
-
|
|
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
|
-
|
|
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,80 +502,68 @@ class Asset(BaseModel):
|
|
|
239
502
|
@dataclass
|
|
240
503
|
class Output:
|
|
241
504
|
"""
|
|
242
|
-
Output of a decision problem.
|
|
243
|
-
some location.
|
|
244
|
-
|
|
245
|
-
The output can be in different formats, such as JSON (default) or
|
|
246
|
-
CSV_ARCHIVE.
|
|
247
|
-
|
|
248
|
-
If you used options, you can also include them in the output, to be
|
|
249
|
-
serialized to the write location.
|
|
505
|
+
Output of a decision problem.
|
|
250
506
|
|
|
251
|
-
|
|
252
|
-
result of the decision problem. The solution's type must match the
|
|
253
|
-
`output_format`:
|
|
507
|
+
You can import the `Output` class directly from `nextmv`:
|
|
254
508
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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.
|
|
509
|
+
```python
|
|
510
|
+
from nextmv import Output
|
|
511
|
+
```
|
|
265
512
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
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.
|
|
270
516
|
|
|
271
|
-
|
|
517
|
+
Parameters
|
|
272
518
|
----------
|
|
273
|
-
options : Optional[Union[Options, dict[str, Any]]]
|
|
519
|
+
options : Optional[Union[Options, dict[str, Any]]], optional
|
|
274
520
|
Options that the `Output` was created with. These options can be of type
|
|
275
|
-
`Options` or a simple dictionary.
|
|
276
|
-
|
|
277
|
-
they are a dictionary, they will be used as is. If the options are not
|
|
278
|
-
provided, an empty dictionary will be used. If the options are of type
|
|
279
|
-
`dict`, then the dictionary should have the following structure:
|
|
280
|
-
```
|
|
281
|
-
{
|
|
282
|
-
"duration": "30",
|
|
283
|
-
"threads": 4,
|
|
284
|
-
}
|
|
285
|
-
```
|
|
286
|
-
output_format : Optional[OutputFormat]
|
|
521
|
+
`Options` or a simple dictionary. Default is None.
|
|
522
|
+
output_format : Optional[OutputFormat], optional
|
|
287
523
|
Format of the output data. Default is `OutputFormat.JSON`.
|
|
288
|
-
solution : Optional[Union[dict[str, Any], dict[str, list[dict[str, Any]]]]
|
|
524
|
+
solution : Optional[Union[dict[str, Any], Any, dict[str, list[dict[str, Any]]]]], optional
|
|
289
525
|
The solution to the decision problem. The type must match the
|
|
290
|
-
`output_format
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
|
316
567
|
"""
|
|
317
568
|
|
|
318
569
|
options: Optional[Union[Options, dict[str, Any]]] = None
|
|
@@ -323,7 +574,8 @@ class Output:
|
|
|
323
574
|
they are a dictionary, they will be used as is. If the options are not
|
|
324
575
|
provided, an empty dictionary will be used. If the options are of type
|
|
325
576
|
`dict`, then the dictionary should have the following structure:
|
|
326
|
-
|
|
577
|
+
|
|
578
|
+
```python
|
|
327
579
|
{
|
|
328
580
|
"duration": "30",
|
|
329
581
|
"threads": 4,
|
|
@@ -369,9 +621,18 @@ class Output:
|
|
|
369
621
|
"""
|
|
370
622
|
|
|
371
623
|
def __post_init__(self):
|
|
372
|
-
"""
|
|
373
|
-
|
|
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
|
|
374
630
|
|
|
631
|
+
Raises
|
|
632
|
+
------
|
|
633
|
+
ValueError
|
|
634
|
+
If the solution is not compatible with the specified output_format.
|
|
635
|
+
"""
|
|
375
636
|
# Capture a snapshot of the options that were used to create the class
|
|
376
637
|
# so even if they are changed later, we have a record of the original.
|
|
377
638
|
init_options = self.options
|
|
@@ -472,20 +733,72 @@ class Output:
|
|
|
472
733
|
|
|
473
734
|
|
|
474
735
|
class OutputWriter:
|
|
475
|
-
"""
|
|
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
|
+
"""
|
|
476
755
|
|
|
477
756
|
def write(self, output: Union[Output, dict[str, Any], BaseModel], *args, **kwargs) -> None:
|
|
478
757
|
"""
|
|
479
|
-
Write the output data.
|
|
480
|
-
"""
|
|
758
|
+
Write the output data.
|
|
481
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.
|
|
770
|
+
|
|
771
|
+
Raises
|
|
772
|
+
------
|
|
773
|
+
NotImplementedError
|
|
774
|
+
This method must be implemented by subclasses.
|
|
775
|
+
"""
|
|
482
776
|
raise NotImplementedError
|
|
483
777
|
|
|
484
778
|
|
|
485
779
|
class LocalOutputWriter(OutputWriter):
|
|
486
780
|
"""
|
|
487
|
-
Class for
|
|
488
|
-
|
|
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")
|
|
489
802
|
"""
|
|
490
803
|
|
|
491
804
|
def _write_json(
|
|
@@ -493,6 +806,18 @@ class LocalOutputWriter(OutputWriter):
|
|
|
493
806
|
output_dict: dict[str, Any],
|
|
494
807
|
path: Optional[str] = None,
|
|
495
808
|
) -> None:
|
|
809
|
+
"""
|
|
810
|
+
Write output in JSON format.
|
|
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
|
+
"""
|
|
496
821
|
json_configurations = {}
|
|
497
822
|
if hasattr(output, "json_configurations") and output.json_configurations is not None:
|
|
498
823
|
json_configurations = output.json_configurations
|
|
@@ -524,6 +849,24 @@ class LocalOutputWriter(OutputWriter):
|
|
|
524
849
|
output_dict: dict[str, Any],
|
|
525
850
|
path: Optional[str] = None,
|
|
526
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
|
+
"""
|
|
527
870
|
dir_path = "output"
|
|
528
871
|
if path is not None and path != "":
|
|
529
872
|
if os.path.isfile(path):
|
|
@@ -567,6 +910,7 @@ class LocalOutputWriter(OutputWriter):
|
|
|
567
910
|
OutputFormat.JSON: _write_json,
|
|
568
911
|
OutputFormat.CSV_ARCHIVE: _write_archive,
|
|
569
912
|
}
|
|
913
|
+
"""Dictionary mapping output formats to writer functions."""
|
|
570
914
|
|
|
571
915
|
def write(
|
|
572
916
|
self,
|
|
@@ -575,40 +919,48 @@ class LocalOutputWriter(OutputWriter):
|
|
|
575
919
|
skip_stdout_reset: bool = False,
|
|
576
920
|
) -> None:
|
|
577
921
|
"""
|
|
578
|
-
Write the
|
|
579
|
-
the `path` parameter, depending on the `Output.output_format`:
|
|
922
|
+
Write the output to the local filesystem or stdout.
|
|
580
923
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
- `OutputFormat.CSV_ARCHIVE`: the `path` is the directory where the CSV
|
|
584
|
-
files will be written. If empty or `None`, the data will be written
|
|
585
|
-
to a directory named `output` under the current working directory.
|
|
586
|
-
The `Output.options` and `Output.statistics` will be written to
|
|
587
|
-
stdout.
|
|
588
|
-
|
|
589
|
-
This function detects if stdout was redirected and resets it to avoid
|
|
590
|
-
unexpected behavior. If you want to skip this behavior, set the
|
|
591
|
-
`skip_stdout_reset` parameter to `True`.
|
|
592
|
-
|
|
593
|
-
If the `output` is a `dict`, it will be simply written to the specified
|
|
594
|
-
`path`, as a passthrough. On the other hand, if the `output` is of type
|
|
595
|
-
`Output`, a more structured object will be written, which adheres to
|
|
596
|
-
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.
|
|
597
926
|
|
|
598
927
|
Parameters
|
|
599
928
|
----------
|
|
600
|
-
output: Output, dict[str, Any]
|
|
601
|
-
Output data to write.
|
|
602
|
-
path : str
|
|
603
|
-
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.
|
|
604
936
|
skip_stdout_reset : bool, optional
|
|
605
|
-
Skip resetting stdout before writing the output data. Default is
|
|
606
|
-
`False`.
|
|
937
|
+
Skip resetting stdout before writing the output data. Default is False.
|
|
607
938
|
|
|
608
939
|
Raises
|
|
609
940
|
------
|
|
610
941
|
ValueError
|
|
611
|
-
If the
|
|
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"})
|
|
612
964
|
"""
|
|
613
965
|
|
|
614
966
|
# If the user forgot to reset stdout after redirecting it, we need to
|
|
@@ -652,42 +1004,50 @@ def write_local(
|
|
|
652
1004
|
skip_stdout_reset: bool = False,
|
|
653
1005
|
) -> None:
|
|
654
1006
|
"""
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
1007
|
+
!!! warning
|
|
1008
|
+
`write_local` is deprecated, use `write` instead.
|
|
1009
|
+
|
|
1010
|
+
Write the output to the local filesystem or stdout.
|
|
658
1011
|
|
|
659
1012
|
This is a convenience function for instantiating a `LocalOutputWriter` and
|
|
660
1013
|
calling its `write` method.
|
|
661
1014
|
|
|
662
|
-
Write the `output` to the local filesystem. Consider the following for the
|
|
663
|
-
`path` parameter, depending on the `Output.output_format`:
|
|
664
|
-
|
|
665
|
-
- `OutputFormat.JSON`: the `path` is the file where the JSON data will
|
|
666
|
-
be written. If empty or `None`, the data will be written to stdout.
|
|
667
|
-
- `OutputFormat.CSV_ARCHIVE`: the `path` is the directory where the CSV
|
|
668
|
-
files will be written. If empty or `None`, the data will be written
|
|
669
|
-
to a directory named `output` under the current working directory.
|
|
670
|
-
The `Output.options` and `Output.statistics` will be written to
|
|
671
|
-
stdout.
|
|
672
|
-
|
|
673
|
-
This function detects if stdout was redirected and resets it to avoid
|
|
674
|
-
unexpected behavior. If you want to skip this behavior, set the
|
|
675
|
-
`skip_stdout_reset` parameter to `True`.
|
|
676
|
-
|
|
677
1015
|
Parameters
|
|
678
1016
|
----------
|
|
679
|
-
output : Output, dict[str, Any]
|
|
680
|
-
Output data to write.
|
|
681
|
-
path : str
|
|
682
|
-
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.
|
|
683
1028
|
skip_stdout_reset : bool, optional
|
|
684
|
-
Skip resetting stdout before writing the output data. Default is
|
|
685
|
-
`False`.
|
|
1029
|
+
Skip resetting stdout before writing the output data. Default is False.
|
|
686
1030
|
|
|
687
1031
|
Raises
|
|
688
1032
|
------
|
|
689
1033
|
ValueError
|
|
690
|
-
If the
|
|
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"})
|
|
691
1051
|
"""
|
|
692
1052
|
|
|
693
1053
|
deprecated(
|
|
@@ -700,6 +1060,7 @@ def write_local(
|
|
|
700
1060
|
|
|
701
1061
|
|
|
702
1062
|
_LOCAL_OUTPUT_WRITER = LocalOutputWriter()
|
|
1063
|
+
"""Default LocalOutputWriter instance used by the write function."""
|
|
703
1064
|
|
|
704
1065
|
|
|
705
1066
|
def write(
|
|
@@ -709,46 +1070,78 @@ def write(
|
|
|
709
1070
|
writer: Optional[OutputWriter] = _LOCAL_OUTPUT_WRITER,
|
|
710
1071
|
) -> None:
|
|
711
1072
|
"""
|
|
712
|
-
|
|
713
|
-
output to the specified destination. The `writer` is used to call the
|
|
714
|
-
`.write` method. Note that the default writes is the `LocalOutputWriter`.
|
|
1073
|
+
Write the output to the specified destination.
|
|
715
1074
|
|
|
716
|
-
|
|
717
|
-
`Output.output_format`:
|
|
1075
|
+
You can import the `write` function directly from `nextmv`:
|
|
718
1076
|
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
files will be written. If empty or `None`, the data will be written
|
|
723
|
-
to a directory named `output` under the current working directory.
|
|
724
|
-
The `Output.options` and `Output.statistics` will be written to
|
|
725
|
-
stdout.
|
|
1077
|
+
```python
|
|
1078
|
+
from nextmv import write
|
|
1079
|
+
```
|
|
726
1080
|
|
|
727
|
-
This
|
|
728
|
-
|
|
729
|
-
`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.
|
|
730
1083
|
|
|
731
1084
|
Parameters
|
|
732
1085
|
----------
|
|
733
|
-
output : Output, dict[str, Any]
|
|
734
|
-
Output data to write.
|
|
735
|
-
path : str
|
|
736
|
-
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.
|
|
737
1097
|
skip_stdout_reset : bool, optional
|
|
738
|
-
Skip resetting stdout before writing the output data. Default is
|
|
739
|
-
|
|
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.
|
|
740
1102
|
|
|
741
1103
|
Raises
|
|
742
1104
|
------
|
|
743
1105
|
ValueError
|
|
744
|
-
If the
|
|
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")
|
|
745
1118
|
"""
|
|
746
1119
|
|
|
747
1120
|
writer.write(output, path, skip_stdout_reset)
|
|
748
1121
|
|
|
749
1122
|
|
|
750
|
-
def _custom_serial(obj: Any):
|
|
751
|
-
"""
|
|
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
|
+
"""
|
|
752
1145
|
|
|
753
1146
|
if isinstance(obj, (datetime.datetime | datetime.date)):
|
|
754
1147
|
return obj.isoformat()
|