nextmv 0.27.0__py3-none-any.whl → 0.28.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
nextmv/input.py CHANGED
@@ -1,4 +1,26 @@
1
- """Module for handling input sources and data."""
1
+ """
2
+ Module for handling input sources and data.
3
+
4
+ This module provides classes and functions for loading and handling input data
5
+ in various formats for decision problems. It supports JSON, plain text, CSV,
6
+ and CSV archive formats and can load data from standard input or files.
7
+
8
+ Classes
9
+ -------
10
+ InputFormat
11
+ Enum defining supported input data formats (JSON, TEXT, CSV, CSV_ARCHIVE).
12
+ Input
13
+ Container for input data with format specification and options.
14
+ InputLoader
15
+ Base class for loading inputs from various sources.
16
+ LocalInputLoader
17
+ Class for loading inputs from local files or stdin.
18
+
19
+ Functions
20
+ ---------
21
+ load
22
+ Load input data using a specified loader.
23
+ """
2
24
 
3
25
  import copy
4
26
  import csv
@@ -14,7 +36,28 @@ from nextmv.options import Options
14
36
 
15
37
 
16
38
  class InputFormat(str, Enum):
17
- """Format of an `Input`."""
39
+ """
40
+ Format of an `Input`.
41
+
42
+ You can import the `InputFormat` class directly from `nextmv`:
43
+
44
+ ```python
45
+ from nextmv import InputFormat
46
+ ```
47
+
48
+ This enum specifies the supported formats for input data.
49
+
50
+ Attributes
51
+ ----------
52
+ JSON : str
53
+ JSON format, utf-8 encoded.
54
+ TEXT : str
55
+ Text format, utf-8 encoded.
56
+ CSV : str
57
+ CSV format, utf-8 encoded.
58
+ CSV_ARCHIVE : str
59
+ CSV archive format: multiple CSV files.
60
+ """
18
61
 
19
62
  JSON = "json"
20
63
  """JSON format, utf-8 encoded."""
@@ -31,9 +74,15 @@ class Input:
31
74
  """
32
75
  Input for a decision problem.
33
76
 
77
+ You can import the `Input` class directly from `nextmv`:
78
+
79
+ ```python
80
+ from nextmv import Input
81
+ ```
82
+
34
83
  Parameters
35
84
  ----------
36
- data : Any
85
+ data : Union[dict[str, Any], str, list[dict[str, Any]], dict[str, list[dict[str, Any]]]]
37
86
  The actual data.
38
87
  input_format : InputFormat, optional
39
88
  Format of the input data. Default is `InputFormat.JSON`.
@@ -47,17 +96,46 @@ class Input:
47
96
  list[dict[str, Any]], # CSV
48
97
  dict[str, list[dict[str, Any]]], # CSV_ARCHIVE
49
98
  ]
50
- """The actual data. The data can be of various types, depending on the
51
- input format."""
99
+ """
100
+ The actual data.
101
+
102
+ The data can be of various types, depending on the input format:
103
+
104
+ - For `JSON`: `Union[dict[str, Any], Any]`
105
+ - For `TEXT`: `str`
106
+ - For `CSV`: `list[dict[str, Any]]`
107
+ - For `CSV_ARCHIVE`: `dict[str, list[dict[str, Any]]]`
108
+ """
52
109
 
53
110
  input_format: Optional[InputFormat] = InputFormat.JSON
54
- """Format of the input data. Default is `InputFormat.JSON`."""
111
+ """
112
+ Format of the input data.
113
+
114
+ Default is `InputFormat.JSON`.
115
+ """
116
+
55
117
  options: Optional[Options] = None
56
- """Options that the `Input` were created with."""
118
+ """
119
+ Options that the `Input` was created with.
120
+
121
+ A copy of the options is made during initialization, ensuring the original
122
+ options remain unchanged even if modified later.
123
+ """
57
124
 
58
125
  def __post_init__(self):
59
- """Check that the data matches the format given to initialize the
60
- class."""
126
+ """
127
+ Check that the data matches the format given to initialize the class.
128
+
129
+ This method is automatically called after the dataclass is initialized.
130
+ It validates that the data provided is of the correct type according to
131
+ the specified input_format and makes a deep copy of the options to ensure
132
+ the input maintains its own copy.
133
+
134
+ Raises
135
+ ------
136
+ ValueError
137
+ If the data type doesn't match the expected type for the given format.
138
+ """
61
139
 
62
140
  if self.input_format == InputFormat.JSON:
63
141
  try:
@@ -96,14 +174,30 @@ class Input:
96
174
  """
97
175
  Convert the input to a dictionary.
98
176
 
99
- Parameters
100
- ----------
101
- None
177
+ This method serializes the Input object to a dictionary format that can be
178
+ easily converted to JSON or other serialization formats.
102
179
 
103
180
  Returns
104
181
  -------
105
182
  dict[str, Any]
106
- The input as a dictionary.
183
+ A dictionary containing the input data, format, and options.
184
+
185
+ The structure is:
186
+ ```python
187
+ {
188
+ "data": <the input data>,
189
+ "input_format": <the input format as a string>,
190
+ "options": <the options as a dictionary or None>
191
+ }
192
+ ```
193
+
194
+ Examples
195
+ --------
196
+ >>> from nextmv.input import Input, InputFormat
197
+ >>> input_obj = Input(data={"key": "value"}, input_format=InputFormat.JSON)
198
+ >>> input_dict = input_obj.to_dict()
199
+ >>> print(input_dict)
200
+ {'data': {'key': 'value'}, 'input_format': 'json', 'options': None}
107
201
  """
108
202
 
109
203
  return {
@@ -114,7 +208,18 @@ class Input:
114
208
 
115
209
 
116
210
  class InputLoader:
117
- """Base class for loading inputs."""
211
+ """
212
+ Base class for loading inputs.
213
+
214
+ You can import the `InputLoader` class directly from `nextmv`:
215
+
216
+ ```python
217
+ from nextmv import InputLoader
218
+ ```
219
+
220
+ This abstract class defines the interface for input loaders. Subclasses must
221
+ implement the `load` method to provide concrete input loading functionality.
222
+ """
118
223
 
119
224
  def load(
120
225
  self,
@@ -154,20 +259,84 @@ class InputLoader:
154
259
 
155
260
  class LocalInputLoader(InputLoader):
156
261
  """
157
- Class for loading local inputs. This class can load input data from the
158
- local filesystem, by using stdin, a file, or a directory, where applicable.
262
+ Class for loading local inputs.
263
+
264
+ You can import the `LocalInputLoader` class directly from `nextmv`:
265
+
266
+ ```python
267
+ from nextmv import LocalInputLoader
268
+ ```
269
+
270
+ This class can load input data from the local filesystem, by using stdin,
271
+ a file, or a directory, where applicable. It supports various input formats
272
+ like JSON, TEXT, CSV, and CSV archive.
273
+
159
274
  Call the `load` method to read the input data.
275
+
276
+ Examples
277
+ --------
278
+ >>> from nextmv.input import LocalInputLoader, InputFormat
279
+ >>> loader = LocalInputLoader()
280
+ >>> # Load JSON from stdin or file
281
+ >>> input_obj = loader.load(input_format=InputFormat.JSON, path="data.json")
282
+ >>> # Load CSV from a file
283
+ >>> input_obj = loader.load(input_format=InputFormat.CSV, path="data.csv")
160
284
  """
161
285
 
162
286
  def _read_text(path: str, _) -> str:
287
+ """
288
+ Read a text file and return its contents.
289
+
290
+ Parameters
291
+ ----------
292
+ path : str
293
+ Path to the text file.
294
+ _ : Any
295
+ Placeholder for unused parameter (for API consistency).
296
+
297
+ Returns
298
+ -------
299
+ str
300
+ Contents of the text file with trailing newlines removed.
301
+ """
163
302
  with open(path, encoding="utf-8") as f:
164
303
  return f.read().rstrip("\n")
165
304
 
166
305
  def _read_csv(path: str, csv_configurations: Optional[dict[str, Any]]) -> list[dict[str, Any]]:
306
+ """
307
+ Read a CSV file and return its contents as a list of dictionaries.
308
+
309
+ Parameters
310
+ ----------
311
+ path : str
312
+ Path to the CSV file.
313
+ csv_configurations : dict[str, Any], optional
314
+ Configuration parameters for the CSV DictReader.
315
+
316
+ Returns
317
+ -------
318
+ list[dict[str, Any]]
319
+ List of dictionaries where each dictionary represents a row in the CSV.
320
+ """
167
321
  with open(path, encoding="utf-8") as f:
168
322
  return list(csv.DictReader(f, **csv_configurations))
169
323
 
170
324
  def _read_json(path: str, _) -> Union[dict[str, Any], Any]:
325
+ """
326
+ Read a JSON file and return its parsed contents.
327
+
328
+ Parameters
329
+ ----------
330
+ path : str
331
+ Path to the JSON file.
332
+ _ : Any
333
+ Placeholder for unused parameter (for API consistency).
334
+
335
+ Returns
336
+ -------
337
+ Union[dict[str, Any], Any]
338
+ Parsed JSON data.
339
+ """
171
340
  with open(path, encoding="utf-8") as f:
172
341
  return json.load(f)
173
342
 
@@ -177,6 +346,13 @@ class LocalInputLoader(InputLoader):
177
346
  InputFormat.TEXT: lambda _: sys.stdin.read().rstrip("\n"),
178
347
  InputFormat.CSV: lambda csv_configurations: list(csv.DictReader(sys.stdin, **csv_configurations)),
179
348
  }
349
+ """
350
+ Dictionary of functions to read from standard input.
351
+
352
+ Each key is an InputFormat, and each value is a function that reads from
353
+ standard input in that format.
354
+ """
355
+
180
356
  # These callbacks were not implemented with lambda because we needed
181
357
  # multiple lines. By using `open`, we needed the `with` to be able to close
182
358
  # the file.
@@ -185,6 +361,12 @@ class LocalInputLoader(InputLoader):
185
361
  InputFormat.TEXT: _read_text,
186
362
  InputFormat.CSV: _read_csv,
187
363
  }
364
+ """
365
+ Dictionary of functions to read from files.
366
+
367
+ Each key is an InputFormat, and each value is a function that reads from
368
+ a file in that format.
369
+ """
188
370
 
189
371
  def load(
190
372
  self,
@@ -256,8 +438,27 @@ class LocalInputLoader(InputLoader):
256
438
  use_file_reader: bool = False,
257
439
  ) -> Union[dict[str, Any], str, list[dict[str, Any]]]:
258
440
  """
259
- Load a utf-8 encoded file. Can come from stdin or a file in the
260
- filesystem.
441
+ Load a utf-8 encoded file from stdin or filesystem.
442
+
443
+ This internal method handles loading data in various formats from either
444
+ standard input or a file.
445
+
446
+ Parameters
447
+ ----------
448
+ csv_configurations : dict[str, Any], optional
449
+ Configuration parameters for the CSV DictReader.
450
+ path : str, optional
451
+ Path to the file to read from. If None or empty, reads from stdin.
452
+ input_format : InputFormat, optional
453
+ Format of the input data. Default is JSON.
454
+ use_file_reader : bool, optional
455
+ Whether to force using the file reader even if path is None.
456
+ Default is False.
457
+
458
+ Returns
459
+ -------
460
+ Union[dict[str, Any], str, list[dict[str, Any]]]
461
+ Data read from stdin or file in the specified format.
261
462
  """
262
463
 
263
464
  # If we forcibly want to use the file reader, we can do so.
@@ -277,7 +478,29 @@ class LocalInputLoader(InputLoader):
277
478
  path: Optional[str] = None,
278
479
  ) -> dict[str, list[dict[str, Any]]]:
279
480
  """
280
- Load files from a directory. Will only load CSV files.
481
+ Load CSV files from a directory.
482
+
483
+ This internal method loads all CSV files from a specified directory,
484
+ organizing them into a dictionary where each key is the filename
485
+ (without .csv extension) and each value is the parsed CSV content.
486
+
487
+ Parameters
488
+ ----------
489
+ csv_configurations : dict[str, Any], optional
490
+ Configuration parameters for the CSV DictReader.
491
+ path : str, optional
492
+ Path to the directory containing CSV files. If None or empty,
493
+ uses "./input" as the default directory.
494
+
495
+ Returns
496
+ -------
497
+ dict[str, list[dict[str, Any]]]
498
+ Dictionary mapping filenames to CSV contents.
499
+
500
+ Raises
501
+ ------
502
+ ValueError
503
+ If the path is not a directory or the default directory doesn't exist.
281
504
  """
282
505
 
283
506
  dir_path = "input"
@@ -312,32 +535,14 @@ def load_local(
312
535
  csv_configurations: Optional[dict[str, Any]] = None,
313
536
  ) -> Input:
314
537
  """
315
- DEPRECATION WARNING
316
- ----------
317
- `load_local` is deprecated, use `load` instead.
538
+ !!! warning
539
+ `load_local` is deprecated, use `load` instead.
540
+
541
+ Load input data from local sources.
318
542
 
319
543
  This is a convenience function for instantiating a `LocalInputLoader`
320
544
  and calling its `load` method.
321
545
 
322
- Load the input data. The input data can be in various formats. For
323
- `InputFormat.JSON`, `InputFormat.TEXT`, and `InputFormat.CSV`, the data can
324
- be streamed from stdin or read from a file. When the `path` argument is
325
- provided (and valid), the input data is read from the file specified by
326
- `path`, otherwise, it is streamed from stdin. For
327
- `InputFormat.CSV_ARCHIVE`, the input data is read from the directory
328
- specified by `path`. If the `path` is not provided, the default location
329
- `input` is used. The directory should contain one or more files, where each
330
- file in the directory is a CSV file.
331
-
332
- The `Input` that is returned contains the `data` attribute. This data can
333
- be of different types, depending on the provided `input_format`:
334
-
335
- - `InputFormat.JSON`: the data is a `dict[str, Any]`.
336
- - `InputFormat.TEXT`: the data is a `str`.
337
- - `InputFormat.CSV`: the data is a `list[dict[str, Any]]`.
338
- - `InputFormat.CSV_ARCHIVE`: the data is a `dict[str, list[dict[str, Any]]]`.
339
- Each key is the name of the CSV file, minus the `.csv` extension.
340
-
341
546
  Parameters
342
547
  ----------
343
548
  input_format : InputFormat, optional
@@ -347,19 +552,22 @@ def load_local(
347
552
  path : str, optional
348
553
  Path to the input data.
349
554
  csv_configurations : dict[str, Any], optional
350
- Configurations for loading CSV files. The default `DictReader` is used
351
- when loading a CSV file, so you have the option to pass in a dictionary
352
- with custom kwargs for the `DictReader`.
555
+ Configurations for loading CSV files. Custom kwargs for
556
+ Python's `csv.DictReader`.
353
557
 
354
558
  Returns
355
559
  -------
356
560
  Input
357
- The input data.
561
+ The loaded input data in an Input object.
358
562
 
359
563
  Raises
360
564
  ------
361
565
  ValueError
362
- If the path is not a directory when working with CSV_ARCHIVE.
566
+ If the path is invalid or data format is incorrect.
567
+
568
+ See Also
569
+ --------
570
+ load : The recommended function to use instead.
363
571
  """
364
572
 
365
573
  deprecated(
@@ -372,6 +580,7 @@ def load_local(
372
580
 
373
581
 
374
582
  _LOCAL_INPUT_LOADER = LocalInputLoader()
583
+ """Default instance of LocalInputLoader used by the load function."""
375
584
 
376
585
 
377
586
  def load(
@@ -382,54 +591,63 @@ def load(
382
591
  loader: Optional[InputLoader] = _LOCAL_INPUT_LOADER,
383
592
  ) -> Input:
384
593
  """
385
- This is a convenience function for loading an `Input`, i.e.: load the input
386
- data. The `loader` is used to call the `.load` method. Note that the
387
- default loader is the `LocalInputLoader`.
388
-
389
- The input data can be in various formats. For
390
- `InputFormat.JSON`, `InputFormat.TEXT`, and `InputFormat.CSV`, the data can
391
- be streamed from stdin or read from a file. When the `path` argument is
392
- provided (and valid), the input data is read from the file specified by
393
- `path`, otherwise, it is streamed from stdin. For
394
- `InputFormat.CSV_ARCHIVE`, the input data is read from the directory
395
- specified by `path`. If the `path` is not provided, the default location
396
- `input` is used. The directory should contain one or more files, where each
397
- file in the directory is a CSV file.
398
-
399
- The `Input` that is returned contains the `data` attribute. This data can
400
- be of different types, depending on the provided `input_format`:
401
-
402
- - `InputFormat.JSON`: the data is a `dict[str, Any]`.
403
- - `InputFormat.TEXT`: the data is a `str`.
404
- - `InputFormat.CSV`: the data is a `list[dict[str, Any]]`.
405
- - `InputFormat.CSV_ARCHIVE`: the data is a `dict[str, list[dict[str, Any]]]`.
594
+ Load input data using the specified loader.
595
+
596
+ You can import the `load` function directly from `nextmv`:
597
+
598
+ ```python
599
+ from nextmv import load
600
+ ```
601
+
602
+ This is a convenience function for loading an `Input` object. By default,
603
+ it uses the `LocalInputLoader` to load data from local sources.
604
+
605
+ The input data can be in various formats and can be loaded from different
606
+ sources depending on the loader:
607
+
608
+ - `InputFormat.JSON`: the data is a `dict[str, Any]`
609
+ - `InputFormat.TEXT`: the data is a `str`
610
+ - `InputFormat.CSV`: the data is a `list[dict[str, Any]]`
611
+ - `InputFormat.CSV_ARCHIVE`: the data is a `dict[str, list[dict[str, Any]]]`
406
612
  Each key is the name of the CSV file, minus the `.csv` extension.
407
613
 
408
614
  Parameters
409
615
  ----------
410
- input_format: InputFormat, optional
616
+ input_format : InputFormat, optional
411
617
  Format of the input data. Default is `InputFormat.JSON`.
412
- options: Options, optional
618
+ options : Options, optional
413
619
  Options for loading the input data.
414
- path: str, optional
415
- Path to the input data.
416
- csv_configurations: dict[str, Any], optional
417
- Configurations for loading CSV files. The default `DictReader` is used
418
- when loading a CSV file, so you have the option to pass in a dictionary
419
- with custom kwargs for the `DictReader`.
420
- loader: InputLoader, optional
421
- The loader to use for loading the input data. Default is
422
- `LocalInputLoader`.
620
+ path : str, optional
621
+ Path to the input data. For file-based loaders:
622
+ - If provided, reads from the specified file or directory
623
+ - If None, typically reads from stdin (for JSON, TEXT, CSV)
624
+ or uses a default directory (for CSV_ARCHIVE)
625
+ csv_configurations : dict[str, Any], optional
626
+ Configurations for loading CSV files. Custom kwargs for
627
+ Python's `csv.DictReader`.
628
+ loader : InputLoader, optional
629
+ The loader to use for loading the input data.
630
+ Default is an instance of `LocalInputLoader`.
423
631
 
424
632
  Returns
425
633
  -------
426
634
  Input
427
- The input data.
635
+ The loaded input data in an Input object.
428
636
 
429
637
  Raises
430
638
  ------
431
639
  ValueError
432
- If the path is not a directory when working with CSV_ARCHIVE.
640
+ If the path is invalid or data format is incorrect.
641
+
642
+ Examples
643
+ --------
644
+ >>> from nextmv.input import load, InputFormat
645
+ >>> # Load JSON from stdin
646
+ >>> input_obj = load(input_format=InputFormat.JSON)
647
+ >>> # Load CSV from a file
648
+ >>> input_obj = load(input_format=InputFormat.CSV, path="data.csv")
649
+ >>> # Load CSV archive from a directory
650
+ >>> input_obj = load(input_format=InputFormat.CSV_ARCHIVE, path="input_dir")
433
651
  """
434
652
 
435
653
  return loader.load(input_format, options, path, csv_configurations)
nextmv/logger.py CHANGED
@@ -1,14 +1,48 @@
1
- """Logger that writes to stderr."""
1
+ """
2
+ Logger module that writes to stderr.
3
+
4
+ This module provides utilities for redirecting standard output to standard error
5
+ and for writing log messages directly to stderr.
6
+
7
+ Functions
8
+ ---------
9
+ redirect_stdout
10
+ Redirect all messages written to stdout to stderr.
11
+ reset_stdout
12
+ Reset stdout to its original value.
13
+ log
14
+ Log a message to stderr.
15
+ """
2
16
 
3
17
  import sys
4
18
 
19
+ # Original stdout reference held when redirection is active
5
20
  __original_stdout = None
21
+ # Flag to track if stdout has been redirected
6
22
  __stdout_redirected = False
7
23
 
8
24
 
9
25
  def redirect_stdout() -> None:
10
- """Redirect all messages written to stdout to stderr. When you do not want
11
- to redirect stdout anymore, call `reset_stdout`."""
26
+ """
27
+ Redirect all messages written to stdout to stderr.
28
+
29
+ You can import the `redirect_stdout` function directly from `nextmv`:
30
+
31
+ ```python
32
+ from nextmv import redirect_stdout
33
+ ```
34
+
35
+ This function captures the current sys.stdout and replaces it with sys.stderr.
36
+ When redirection is no longer needed, call `reset_stdout()` to restore the
37
+ original stdout.
38
+
39
+ Examples
40
+ --------
41
+ >>> redirect_stdout()
42
+ >>> print("This will go to stderr")
43
+ >>> reset_stdout()
44
+ >>> print("This will go to stdout")
45
+ """
12
46
 
13
47
  global __original_stdout, __stdout_redirected
14
48
  if __stdout_redirected:
@@ -20,9 +54,26 @@ def redirect_stdout() -> None:
20
54
 
21
55
 
22
56
  def reset_stdout() -> None:
23
- """Reset stdout to its original value. This function should always be
24
- called after `redirect_stdout` to avoid unexpected behavior."""
57
+ """
58
+ Reset stdout to its original value.
59
+
60
+ You can import the `reset_stdout` function directly from `nextmv`:
61
+
62
+ ```python
63
+ from nextmv import reset_stdout
64
+ ```
65
+
66
+ This function should always be called after `redirect_stdout()` to avoid
67
+ unexpected behavior. It restores the original stdout that was captured
68
+ during redirection.
25
69
 
70
+ Examples
71
+ --------
72
+ >>> redirect_stdout()
73
+ >>> print("This will go to stderr")
74
+ >>> reset_stdout()
75
+ >>> print("This will go to stdout")
76
+ """
26
77
  global __original_stdout, __stdout_redirected
27
78
  if not __stdout_redirected:
28
79
  return
@@ -40,8 +91,21 @@ def log(message: str) -> None:
40
91
  """
41
92
  Log a message to stderr.
42
93
 
43
- Args:
44
- message: The message to log.
94
+ You can import the `log` function directly from `nextmv`:
95
+
96
+ ```python
97
+ from nextmv import log
98
+ ```
99
+
100
+ Parameters
101
+ ----------
102
+ message : str
103
+ The message to log.
104
+
105
+ Examples
106
+ --------
107
+ >>> log("An error occurred")
108
+ An error occurred
45
109
  """
46
110
 
47
111
  print(message, file=sys.stderr)