nextmv 0.28.4__py3-none-any.whl → 0.29.0.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/output.py CHANGED
@@ -21,6 +21,8 @@ Statistics
21
21
  Complete statistics container for a solution, including run metrics and result data.
22
22
  OutputFormat
23
23
  Enumeration of supported output formats.
24
+ SolutionFile
25
+ Represents a solution to be written as a file.
24
26
  VisualSchema
25
27
  Enumeration of supported visualization schemas.
26
28
  Visual
@@ -44,6 +46,7 @@ import copy
44
46
  import csv
45
47
  import os
46
48
  import sys
49
+ from collections.abc import Callable
47
50
  from dataclasses import dataclass
48
51
  from enum import Enum
49
52
  from typing import Any, Optional, Union
@@ -297,33 +300,6 @@ class Statistics(BaseModel):
297
300
  """Schema (version). This class only supports `v1`."""
298
301
 
299
302
 
300
- class OutputFormat(str, Enum):
301
- """
302
- Enumeration of supported output formats.
303
-
304
- You can import the `OutputFormat` class directly from `nextmv`:
305
-
306
- ```python
307
- from nextmv import OutputFormat
308
- ```
309
-
310
- This enum defines the different formats that can be used for outputting data.
311
- Each format has specific requirements and behaviors when writing.
312
-
313
- Attributes
314
- ----------
315
- JSON : str
316
- JSON format, utf-8 encoded.
317
- CSV_ARCHIVE : str
318
- CSV archive format: multiple CSV files.
319
- """
320
-
321
- JSON = "json"
322
- """JSON format, utf-8 encoded."""
323
- CSV_ARCHIVE = "csv-archive"
324
- """CSV archive format: multiple CSV files."""
325
-
326
-
327
303
  class VisualSchema(str, Enum):
328
304
  """
329
305
  Enumeration of supported visualization schemas.
@@ -498,6 +474,326 @@ class Asset(BaseModel):
498
474
  raise ValueError(f"unsupported content_type: {self.content_type}, supported types are `json`")
499
475
 
500
476
 
477
+ class OutputFormat(str, Enum):
478
+ """
479
+ Enumeration of supported output formats.
480
+
481
+ You can import the `OutputFormat` class directly from `nextmv`:
482
+
483
+ ```python
484
+ from nextmv import OutputFormat
485
+ ```
486
+
487
+ This enum defines the different formats that can be used for outputting data.
488
+ Each format has specific requirements and behaviors when writing.
489
+
490
+ Attributes
491
+ ----------
492
+ JSON : str
493
+ JSON format, utf-8 encoded.
494
+ CSV_ARCHIVE : str
495
+ CSV archive format: multiple CSV files.
496
+ MULTI_FILE : str
497
+ Multi-file format: multiple files in a directory.
498
+ """
499
+
500
+ JSON = "json"
501
+ """JSON format, utf-8 encoded."""
502
+ CSV_ARCHIVE = "csv-archive"
503
+ """CSV archive format: multiple CSV files."""
504
+ MULTI_FILE = "multi-file"
505
+ """Multi-file format: multiple files in a directory."""
506
+
507
+
508
+ @dataclass
509
+ class SolutionFile:
510
+ """
511
+ Represents a solution to be written as a file.
512
+
513
+ You can import the `SolutionFile` class directly from `nextmv`:
514
+
515
+ ```python
516
+ from nextmv import SolutionFile
517
+ ```
518
+
519
+ This class is used to define a solution that will be written to a file in
520
+ the filesystem. It includes the name of the file, the data to be written,
521
+ and the writer function that will handle the serialization of the data.
522
+ This `SolutionFile` class is typically used in the `Output`, when the
523
+ `Output.output_format` is set to `OutputFormat.MULTI_FILE`. Given that it
524
+ is difficult to handle every edge case of how a solution is serialized, and
525
+ written to a file, this class exists so that the user can implement the
526
+ `writer` callable of their choice and provide it with any `writer_args`
527
+ and `writer_kwargs` they might need.
528
+
529
+ Parameters
530
+ ----------
531
+ name : str
532
+ Name of the output file. The file extension should be included in the
533
+ name.
534
+ data : Any
535
+ The actual data that will be written to the file. This can be any type
536
+ that can be given to the `writer` function. For example, if the `writer`
537
+ is a `csv.DictWriter`, then the data should be a list of dictionaries,
538
+ where each dictionary represents a row in the CSV file.
539
+ writer : Callable
540
+ Callable that writes the solution data to the file. This should be a
541
+ function implemented by the user. There are convenience functions that you
542
+ can use as a writer as well. The `writer` must receive, at the very
543
+ minimum, the following arguments:
544
+
545
+ - `file_path`: a `str` argument which is the location where this solution
546
+ will be written to. This includes the dir and the name of the file. As
547
+ such, the `name` parameter of this class is going to be passed to this
548
+ function joined with the directory where the file will be written.
549
+ - `data`: the actual data that will be written to the file. This can be any
550
+ type that can be given to the `writer` function. The `data` parameter of
551
+ this class is going to be passed to the `writer` function.
552
+
553
+ The `writer` can also receive additional arguments, and keyword arguments.
554
+ The `writer_args` and `writer_kwargs` parameters of this class can be used
555
+ to provide those additional arguments.
556
+ writer_args : Optional[list[Any]], optional
557
+ Positional arguments to pass to the writer function.
558
+ writer_kwargs : Optional[dict[str, Any]], optional
559
+ Keyword arguments to pass to the writer function.
560
+
561
+ Examples
562
+ --------
563
+ >>> from nextmv import SolutionFile
564
+ >>> solution_file = SolutionFile(
565
+ ... name="solution.csv",
566
+ ... data=[{"id": 1, "value": 100}, {"id": 2, "value": 200}],
567
+ ... writer=csv.DictWriter,
568
+ ... writer_kwargs={"fieldnames": ["id", "value"]},
569
+ ... writer_args=[open("solution.csv", "w", newline="")],
570
+ ... )
571
+ """
572
+
573
+ name: str
574
+ """
575
+ Name of the solution (output) file. The file extension should be included in the
576
+ name.
577
+ """
578
+ data: Any
579
+ """
580
+ The actual data that will be written to the file. This can be any type that
581
+ can be given to the `writer` function. For example, if the `writer` is a
582
+ `csv.DictWriter`, then the data should be a list of dictionaries, where
583
+ each dictionary represents a row in the CSV file.
584
+ """
585
+ writer: Callable[[str, str, Any], None]
586
+ """
587
+ Callable that writes the solution data to the file. This should be a
588
+ function implemented by the user. There are convenience functions that you
589
+ can use as a writer as well. The `writer` must receive, at the very
590
+ minimum, the following arguments:
591
+
592
+ - `file_path`: a `str` argument which is the location where this solution
593
+ will be written to. This includes the dir and the name of the file. As
594
+ such, the `name` parameter of this class is going to be passed to this
595
+ function joined with the directory where the file will be written.
596
+ - `data`: the actual data that will be written to the file. This can be any
597
+ type that can be given to the `writer` function. The `data` parameter of
598
+ this class is going to be passed to the `writer` function.
599
+
600
+ The `writer` can also receive additional arguments, and keyword arguments.
601
+ The `writer_args` and `writer_kwargs` parameters of this class can be used
602
+ to provide those additional arguments.
603
+ """
604
+ writer_args: Optional[list[Any]] = None
605
+ """
606
+ Optional positional arguments to pass to the writer function. This can be
607
+ used to customize the behavior of the writer.
608
+ """
609
+ writer_kwargs: Optional[dict[str, Any]] = None
610
+ """
611
+ Optional keyword arguments to pass to the writer function. This can be used
612
+ to customize the behavior of the writer.
613
+ """
614
+
615
+
616
+ def json_solution_file(
617
+ name: str,
618
+ data: dict[str, Any],
619
+ json_configurations: Optional[dict[str, Any]] = None,
620
+ ) -> SolutionFile:
621
+ """
622
+ This is a convenience function to build a `SolutionFile`. It writes the
623
+ given `data` to a `.json` file with the provided `name`.
624
+
625
+ You can import this function directly from `nextmv`:
626
+
627
+ ```python
628
+ from nextmv import json_solution_file
629
+ ```
630
+
631
+ Parameters
632
+ ----------
633
+ name : str
634
+ Name of the output file. You don't need to include the `.json`
635
+ extension.
636
+ data : dict[str, Any]
637
+ The actual data that will be written to the file. This should be a
638
+ dictionary that can be serialized to JSON.
639
+ json_configurations : Optional[dict[str, Any]], optional
640
+ Optional configuration options for the JSON serialization process. You
641
+ can use these options to configure parameters such as indentation.
642
+
643
+ Returns
644
+ -------
645
+ SolutionFile
646
+ The constructed `SolutionFile` object.
647
+
648
+ Examples
649
+ --------
650
+ >>> from nextmv import json_solution_file
651
+ >>> solution_file = json_solution_file(
652
+ ... name="solution",
653
+ ... data={"id": 1, "value": 100}
654
+ ... )
655
+ >>> solution_file.name
656
+ 'solution.json'
657
+ >>> solution_file.data
658
+ {'id': 1, 'value': 100}
659
+ """
660
+
661
+ if not name.endswith(".json"):
662
+ name += ".json"
663
+
664
+ json_configurations = json_configurations or {}
665
+
666
+ def writer(file_path: str, write_data: dict[str, Any]) -> None:
667
+ serialized = serialize_json(write_data, json_configurations=json_configurations)
668
+
669
+ with open(file_path, "w", encoding="utf-8") as file:
670
+ file.write(serialized + "\n")
671
+
672
+ return SolutionFile(
673
+ name=name,
674
+ data=data,
675
+ writer=writer,
676
+ )
677
+
678
+
679
+ def csv_solution_file(
680
+ name: str,
681
+ data: list[dict[str, Any]],
682
+ csv_configurations: Optional[dict[str, Any]] = None,
683
+ ) -> SolutionFile:
684
+ """
685
+ This is a convenience function to build a `SolutionFile`. It writes the
686
+ given `data` to a `.csv` file with the provided `name`.
687
+
688
+ You can import this function directly from `nextmv`:
689
+
690
+ ```python
691
+ from nextmv import csv_solution_file
692
+ ```
693
+
694
+ Parameters
695
+ ----------
696
+ name : str
697
+ Name of the output file. You don't need to include the `.csv`
698
+ extension.
699
+ data : list[dict[str, Any]]
700
+ The actual data that will be written to the file. This should be a list
701
+ of dictionaries, where each dictionary represents a row in the CSV file.
702
+ The keys of the dictionaries will be used as the column headers in the
703
+ CSV file.
704
+ csv_configurations : Optional[dict[str, Any]], optional
705
+ Optional configuration options for the CSV serialization process.
706
+
707
+ Returns
708
+ -------
709
+ SolutionFile
710
+ The constructed `SolutionFile` object.
711
+
712
+ Examples
713
+ --------
714
+ >>> from nextmv import csv_solution_file
715
+ >>> solution_file = csv_solution_file(
716
+ ... name="solution",
717
+ ... data=[{"id": 1, "value": 100}, {"id": 2, "value": 200}]
718
+ ... )
719
+ >>> solution_file.name
720
+ 'solution.csv'
721
+ >>> solution_file.data
722
+ [{'id': 1, 'value': 100}, {'id': 2, 'value': 200}]
723
+ """
724
+
725
+ if not name.endswith(".csv"):
726
+ name += ".csv"
727
+
728
+ csv_configurations = csv_configurations or {}
729
+
730
+ def writer(file_path: str, write_data: list[dict[str, Any]]) -> None:
731
+ with open(file_path, "w", encoding="utf-8", newline="") as file:
732
+ writer = csv.DictWriter(
733
+ file,
734
+ fieldnames=write_data[0].keys(),
735
+ **csv_configurations,
736
+ )
737
+ writer.writeheader()
738
+ writer.writerows(write_data)
739
+
740
+ return SolutionFile(
741
+ name=name,
742
+ data=data,
743
+ writer=writer,
744
+ )
745
+
746
+
747
+ def text_solution_file(name: str, data: str) -> SolutionFile:
748
+ """
749
+ This is a convenience function to build a `SolutionFile`. It writes the
750
+ given `data` to a utf-8 encoded file with the provided `name`.
751
+
752
+ You can import this function directly from `nextmv`:
753
+
754
+ ```python
755
+ from nextmv import text_solution_file
756
+ ```
757
+
758
+ You must provide the extension as part of the `name` parameter.
759
+
760
+ Parameters
761
+ ----------
762
+ name : str
763
+ Name of the output file. The file extension must be provided in the
764
+ name.
765
+ data : str
766
+ The actual data that will be written to the file.
767
+
768
+ Returns
769
+ -------
770
+ SolutionFile
771
+ The constructed `SolutionFile` object.
772
+
773
+ Examples
774
+ --------
775
+ >>> from nextmv import text_solution_file
776
+ >>> solution_file = text_solution_file(
777
+ ... name="solution.txt",
778
+ ... data="This is a sample text solution."
779
+ ... )
780
+ >>> solution_file.name
781
+ 'solution.txt'
782
+ >>> solution_file.data
783
+ 'This is a sample text solution.'
784
+ """
785
+
786
+ def writer(file_path: str, write_data: str) -> None:
787
+ with open(file_path, "w", encoding="utf-8") as file:
788
+ file.write(write_data + "\n")
789
+
790
+ return SolutionFile(
791
+ name=name,
792
+ data=data,
793
+ writer=writer,
794
+ )
795
+
796
+
501
797
  @dataclass
502
798
  class Output:
503
799
  """
@@ -513,6 +809,34 @@ class Output:
513
809
  can later be written to various destinations. It supports different output
514
810
  formats and allows for customization of the serialization process.
515
811
 
812
+ The `solution`'s type must match the `output_format`:
813
+
814
+ - `OutputFormat.JSON`: the data must be `dict[str, Any]` or `Any`.
815
+ - `OutputFormat.CSV_ARCHIVE`: the data must be `dict[str, list[dict[str,
816
+ Any]]]`. The keys represent the file names where the data should be
817
+ written. The values are lists of dictionaries, where each dictionary
818
+ represents a row in the CSV file.
819
+
820
+ If you are working with `OutputFormat.MULTI_FILE`, you should use
821
+ `solution_files` instead of `solution`. When `solution_files` is not
822
+ `None`, then the `output_format` _must_ be `OutputFormat.MULTI_FILE`.
823
+ `solution_files` is a list of `SolutionFile` objects, which allows you to
824
+ define the name of the file, the data to be written, and the writer
825
+ function that will handle the serialization of the data. This is useful when
826
+ you need to write the solution to multiple files with different formats or
827
+ configurations.
828
+
829
+ There are convenience functions to create `SolutionFile` objects for
830
+ common use cases, such as:
831
+
832
+ - `json_solution_file`: for writing JSON data to a file.
833
+ - `csv_solution_file`: for writing CSV data to a file.
834
+ - `text_solution_file`: for writing utf-8 encoded data to a file.
835
+
836
+ For other data types, such as Excel, you can create your own `SolutionFile`
837
+ objects by providing a `name`, `data`, and a `writer` function that will
838
+ handle the serialization of the data.
839
+
516
840
  Parameters
517
841
  ----------
518
842
  options : Optional[Union[Options, dict[str, Any]]], optional
@@ -539,15 +863,6 @@ class Output:
539
863
  TypeError
540
864
  If options, statistics, or assets have unsupported types.
541
865
 
542
- Notes
543
- -----
544
- The solution's type must match the `output_format`:
545
-
546
- - `OutputFormat.JSON`: the data must be `dict[str, Any]` or `Any`.
547
- - `OutputFormat.CSV_ARCHIVE`: the data must be `dict[str, list[dict[str, Any]]]`.
548
- The keys represent the file names where the data should be written. The values
549
- are lists of dictionaries, where each dictionary represents a row in the CSV file.
550
-
551
866
  Examples
552
867
  --------
553
868
  >>> from nextmv.output import Output, OutputFormat, Statistics, RunStatistics
@@ -582,14 +897,31 @@ class Output:
582
897
  ```
583
898
  """
584
899
  output_format: Optional[OutputFormat] = OutputFormat.JSON
585
- """Format of the output data. Default is `OutputFormat.JSON`."""
900
+ """
901
+ Format of the output data. Default is `OutputFormat.JSON`. When set to
902
+ `OutputFormat.MULTI_FILE`, the `solution_files` field must be specified and
903
+ cannot be `None`.
904
+ """
586
905
  solution: Optional[
587
906
  Union[
588
907
  Union[dict[str, Any], Any], # JSON
589
908
  dict[str, list[dict[str, Any]]], # CSV_ARCHIVE
590
909
  ]
591
910
  ] = None
592
- """The solution to the decision problem."""
911
+ """
912
+ The solution to the decision problem. Use this filed when working with
913
+ `output_format` of types:
914
+
915
+ - `OutputFormat.JSON`: the data must be `dict[str, Any]` or `Any`.
916
+ - `OutputFormat.CSV_ARCHIVE`: the data must be `dict[str, list[dict[str,
917
+ Any]]]`. The keys represent the file names where the data will be written
918
+ to. The values are lists of dictionaries, where each dictionary represents
919
+ a row in the CSV file.
920
+
921
+ Note that when the `output_format` is set to `OutputFormat.MULTI_FILE`,
922
+ this `solution` field is ignored, as you should use the `solution_files`
923
+ field instead.
924
+ """
593
925
  statistics: Optional[Union[Statistics, dict[str, Any]]] = None
594
926
  """
595
927
  Statistics of the solution. These statistics can be of type `Statistics` or a
@@ -618,6 +950,28 @@ class Output:
618
950
  dictionary, they will be used as is. If the assets are not provided, an
619
951
  empty list will be used.
620
952
  """
953
+ solution_files: Optional[list[SolutionFile]] = None
954
+ """
955
+ Optional list of solution files to be included in the output. These files
956
+ are of type `SolutionFile`, which allows for custom serialization and
957
+ writing of the solution data to files. When this field is specified, then
958
+ the `output_format` must be set to `OutputFormat.MULTI_FILE`, otherwise an
959
+ exception will be raised. The `SolutionFile` class allows you to define the
960
+ name of the file, the data to be written, and the writer function that will
961
+ handle the serialization of the data. This is useful when you need to write
962
+ the solution to multiple files with different formats or configurations.
963
+
964
+ There are convenience functions to create `SolutionFile` objects for
965
+ common use cases, such as:
966
+
967
+ - `json_solution_file`: for writing JSON data to a file.
968
+ - `csv_solution_file`: for writing CSV data to a file.
969
+ - `text_solution_file`: for writing utf-8 encoded data to a file.
970
+
971
+ For other data types, such as Excel, you can create your own `SolutionFile`
972
+ objects by providing a `name`, `data`, and a `writer` function that will
973
+ handle the serialization of the data.
974
+ """
621
975
 
622
976
  def __post_init__(self):
623
977
  """
@@ -638,22 +992,31 @@ class Output:
638
992
  new_options = copy.deepcopy(init_options)
639
993
  self.options = new_options
640
994
 
641
- if self.solution is None:
642
- return
643
-
644
- if self.output_format == OutputFormat.JSON:
645
- try:
646
- _ = serialize_json(self.solution)
647
- except (TypeError, OverflowError) as e:
995
+ if self.solution is not None:
996
+ if self.output_format == OutputFormat.JSON:
997
+ try:
998
+ _ = serialize_json(self.solution)
999
+ except (TypeError, OverflowError) as e:
1000
+ raise ValueError(
1001
+ f"Output has output_format OutputFormat.JSON and "
1002
+ f"Output.solution is of type {type(self.solution)}, which is not JSON serializable"
1003
+ ) from e
1004
+
1005
+ elif self.output_format == OutputFormat.CSV_ARCHIVE and not isinstance(self.solution, dict):
648
1006
  raise ValueError(
649
- f"Output has output_format OutputFormat.JSON and "
650
- f"Output.solution is of type {type(self.solution)}, which is not JSON serializable"
651
- ) from e
1007
+ f"unsupported Output.solution type: {type(self.solution)} with "
1008
+ "output_format OutputFormat.CSV_ARCHIVE, supported type is `dict`"
1009
+ )
652
1010
 
653
- elif self.output_format == OutputFormat.CSV_ARCHIVE and not isinstance(self.solution, dict):
1011
+ if self.solution_files is not None and self.output_format != OutputFormat.MULTI_FILE:
654
1012
  raise ValueError(
655
- f"unsupported Output.solution type: {type(self.solution)} with "
656
- "output_format OutputFormat.CSV_ARCHIVE, supported type is `dict`"
1013
+ f"`solution_files` are not `None`, but `output_format` is different from `OutputFormat.MULTI_FILE`: "
1014
+ f"{self.output_format}. If you want to use `solution_files`, set `output_format` "
1015
+ "to `OutputFormat.MULTI_FILE`."
1016
+ )
1017
+ elif self.solution_files is not None and not isinstance(self.solution_files, list):
1018
+ raise TypeError(
1019
+ f"unsupported Output.solution_files type: {type(self.solution_files)}, supported type is `list`"
657
1020
  )
658
1021
 
659
1022
  def to_dict(self) -> dict[str, Any]: # noqa: C901
@@ -795,6 +1158,7 @@ class LocalOutputWriter(OutputWriter):
795
1158
  """
796
1159
 
797
1160
  def _write_json(
1161
+ self,
798
1162
  output: Union[Output, dict[str, Any], BaseModel],
799
1163
  output_dict: dict[str, Any],
800
1164
  path: Optional[str] = None,
@@ -828,6 +1192,7 @@ class LocalOutputWriter(OutputWriter):
828
1192
  file.write(serialized + "\n")
829
1193
 
830
1194
  def _write_archive(
1195
+ self,
831
1196
  output: Union[Output, dict[str, Any], BaseModel],
832
1197
  output_dict: dict[str, Any],
833
1198
  path: Optional[str] = None,
@@ -892,10 +1257,128 @@ class LocalOutputWriter(OutputWriter):
892
1257
  writer.writeheader()
893
1258
  writer.writerows(data)
894
1259
 
1260
+ def _write_multi_file(
1261
+ self,
1262
+ output: Union[Output, dict[str, Any], BaseModel],
1263
+ output_dict: dict[str, Any],
1264
+ path: Optional[str] = None,
1265
+ ) -> None:
1266
+ """
1267
+ Write output to multiple files.
1268
+
1269
+ Parameters
1270
+ ----------
1271
+ output : Union[Output, dict[str, Any], BaseModel]
1272
+ The output object containing configuration and solution data.
1273
+ output_dict : dict[str, Any]
1274
+ Dictionary representation of the output to write.
1275
+ path : str, optional
1276
+ Directory path to write the CSV files. If None or empty,
1277
+ writes to a directory named "output" in the current working directory.
1278
+
1279
+ Raises
1280
+ ------
1281
+ ValueError
1282
+ If the path is an existing file instead of a directory.
1283
+ """
1284
+ dir_path = "outputs"
1285
+ if path is not None and path != "":
1286
+ if os.path.isfile(path):
1287
+ raise ValueError(f"The path refers to an existing file: {path}")
1288
+
1289
+ dir_path = path
1290
+
1291
+ if not os.path.exists(dir_path):
1292
+ os.makedirs(dir_path)
1293
+
1294
+ json_configurations = {}
1295
+ if hasattr(output, "json_configurations") and output.json_configurations is not None:
1296
+ json_configurations = output.json_configurations
1297
+
1298
+ self._write_multi_file_element(
1299
+ parent_dir=dir_path,
1300
+ json_configurations=json_configurations,
1301
+ output_dict=output_dict,
1302
+ element_key="statistics",
1303
+ )
1304
+ self._write_multi_file_element(
1305
+ parent_dir=dir_path,
1306
+ json_configurations=json_configurations,
1307
+ output_dict=output_dict,
1308
+ element_key="assets",
1309
+ )
1310
+ self._write_multi_file_solution(dir_path=dir_path, output=output)
1311
+
1312
+ def _write_multi_file_element(
1313
+ self,
1314
+ parent_dir: str,
1315
+ output_dict: dict[str, Any],
1316
+ element_key: str,
1317
+ json_configurations: Optional[dict[str, Any]] = None,
1318
+ ):
1319
+ """
1320
+ Auxiliary function to write a specific element of the output
1321
+ dictionary to a file in the specified parent directory.
1322
+ """
1323
+
1324
+ element = output_dict.get(element_key)
1325
+ if element is None or not element:
1326
+ return
1327
+
1328
+ final_dir = os.path.join(parent_dir, element_key)
1329
+
1330
+ if not os.path.exists(final_dir):
1331
+ os.makedirs(final_dir)
1332
+
1333
+ serialized = serialize_json(element, json_configurations=json_configurations)
1334
+
1335
+ with open(os.path.join(final_dir, f"{element_key}.json"), "w", encoding="utf-8") as file:
1336
+ file.write(serialized + "\n")
1337
+
1338
+ def _write_multi_file_solution(
1339
+ self,
1340
+ dir_path: str,
1341
+ output: Output,
1342
+ ):
1343
+ """
1344
+ Auxiliary function to write the solution files to the specified
1345
+ directory.
1346
+ """
1347
+
1348
+ if output.solution_files is None:
1349
+ return
1350
+
1351
+ solutions_dir = os.path.join(dir_path, "solutions")
1352
+
1353
+ if not os.path.exists(solutions_dir):
1354
+ os.makedirs(solutions_dir)
1355
+
1356
+ for solution_file in output.solution_files:
1357
+ if not isinstance(solution_file, SolutionFile):
1358
+ raise TypeError(
1359
+ f"unsupported solution_file type: {type(solution_file)}, supported type is `SolutionFile`"
1360
+ )
1361
+
1362
+ file_path = os.path.join(solutions_dir, solution_file.name)
1363
+ if solution_file.writer_args is None:
1364
+ solution_file.writer_args = []
1365
+ if solution_file.writer_kwargs is None:
1366
+ solution_file.writer_kwargs = {}
1367
+
1368
+ # Call the writer function with the final path, and user provided
1369
+ # arguments and keyword arguments.
1370
+ solution_file.writer(
1371
+ file_path,
1372
+ solution_file.data,
1373
+ *solution_file.writer_args,
1374
+ **solution_file.writer_kwargs,
1375
+ )
1376
+
895
1377
  # Callback functions for writing the output data.
896
1378
  FILE_WRITERS = {
897
1379
  OutputFormat.JSON: _write_json,
898
1380
  OutputFormat.CSV_ARCHIVE: _write_archive,
1381
+ OutputFormat.MULTI_FILE: _write_multi_file,
899
1382
  }
900
1383
  """Dictionary mapping output formats to writer functions."""
901
1384
 
@@ -979,6 +1462,7 @@ class LocalOutputWriter(OutputWriter):
979
1462
  )
980
1463
 
981
1464
  self.FILE_WRITERS[output_format](
1465
+ self,
982
1466
  output=output,
983
1467
  output_dict=output_dict,
984
1468
  path=path,
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nextmv
3
- Version: 0.28.4
3
+ Version: 0.29.0.dev0
4
4
  Summary: The all-purpose Python SDK for Nextmv
5
5
  Project-URL: Homepage, https://www.nextmv.io
6
- Project-URL: Documentation, https://www.nextmv.io/docs/python-sdks/nextmv/installation
6
+ Project-URL: Documentation, https://nextmv-py.docs.nextmv.io/en/latest/nextmv/
7
7
  Project-URL: Repository, https://github.com/nextmv-io/nextmv-py
8
8
  Author-email: Nextmv <tech@nextmv.io>
9
9
  Maintainer-email: Nextmv <tech@nextmv.io>