owlplanner 2025.12.5__py3-none-any.whl → 2026.1.26__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.
Files changed (38) hide show
  1. owlplanner/In Discussion #58, the case of Kim and Sam.md +307 -0
  2. owlplanner/__init__.py +20 -1
  3. owlplanner/abcapi.py +24 -23
  4. owlplanner/cli/README.md +50 -0
  5. owlplanner/cli/_main.py +52 -0
  6. owlplanner/cli/cli_logging.py +56 -0
  7. owlplanner/cli/cmd_list.py +83 -0
  8. owlplanner/cli/cmd_run.py +86 -0
  9. owlplanner/config.py +315 -136
  10. owlplanner/data/__init__.py +21 -0
  11. owlplanner/data/awi.csv +75 -0
  12. owlplanner/data/bendpoints.csv +49 -0
  13. owlplanner/data/newawi.csv +75 -0
  14. owlplanner/data/rates.csv +99 -98
  15. owlplanner/debts.py +315 -0
  16. owlplanner/fixedassets.py +288 -0
  17. owlplanner/mylogging.py +157 -25
  18. owlplanner/plan.py +1044 -332
  19. owlplanner/plotting/__init__.py +16 -3
  20. owlplanner/plotting/base.py +17 -3
  21. owlplanner/plotting/factory.py +16 -3
  22. owlplanner/plotting/matplotlib_backend.py +30 -7
  23. owlplanner/plotting/plotly_backend.py +33 -10
  24. owlplanner/progress.py +66 -9
  25. owlplanner/rates.py +366 -361
  26. owlplanner/socialsecurity.py +142 -22
  27. owlplanner/tax2026.py +170 -57
  28. owlplanner/timelists.py +316 -32
  29. owlplanner/utils.py +204 -5
  30. owlplanner/version.py +20 -1
  31. {owlplanner-2025.12.5.dist-info → owlplanner-2026.1.26.dist-info}/METADATA +50 -158
  32. owlplanner-2026.1.26.dist-info/RECORD +36 -0
  33. owlplanner-2026.1.26.dist-info/entry_points.txt +2 -0
  34. owlplanner-2026.1.26.dist-info/licenses/AUTHORS +15 -0
  35. owlplanner/tax2025.py +0 -339
  36. owlplanner-2025.12.5.dist-info/RECORD +0 -24
  37. {owlplanner-2025.12.5.dist-info → owlplanner-2026.1.26.dist-info}/WHEEL +0 -0
  38. {owlplanner-2025.12.5.dist-info → owlplanner-2026.1.26.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,83 @@
1
+ """
2
+ CLI command for listing retirement planning case files.
3
+
4
+ This module provides the 'list' command for discovering and displaying
5
+ information about retirement planning case files in a directory.
6
+
7
+ Copyright (C) 2025-2026 The Owlplanner Authors
8
+
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
13
+
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
18
+
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
21
+ """
22
+
23
+ import click
24
+ from pathlib import Path
25
+ from loguru import logger
26
+
27
+ import owlplanner as owl
28
+
29
+
30
+ @click.command(name="list")
31
+ @click.argument(
32
+ "directory",
33
+ type=click.Path(exists=True, file_okay=False, path_type=Path),
34
+ default=".",
35
+ )
36
+ def cmd_list(directory):
37
+ """
38
+ List OWL plan files in a directory.
39
+
40
+ DIRECTORY defaults to the current directory.
41
+ """
42
+ logger.debug(f"Listing plans in directory: {directory}")
43
+
44
+ toml_files = sorted(directory.glob("*.toml"))
45
+
46
+ if not toml_files:
47
+ click.echo("No .toml files found.")
48
+ return
49
+
50
+ plans = []
51
+
52
+ for filename in toml_files:
53
+ try:
54
+ logger.debug(f"Loading plan from {filename}")
55
+ plan = owl.readConfig(str(filename), logstreams="loguru", readContributions=False)
56
+ plans.append((filename.stem, plan))
57
+ except Exception as e:
58
+ logger.warning(f"Failed to load {filename}: {e}")
59
+
60
+ if not plans:
61
+ click.echo("No valid OWL plans found.")
62
+ return
63
+
64
+ click.echo(f"{'FILE':<30} {'PLAN NAME':<20} {' TIME LISTS FILE':<30}")
65
+ click.echo("-" * 80)
66
+
67
+ CHECK = "✓"
68
+ CROSS = "✗"
69
+
70
+ plan_dir = directory
71
+
72
+ for stem, plan in plans:
73
+ # Truncate plan name if needed
74
+ plan_name = plan._name
75
+ if len(plan_name) > 20:
76
+ plan_name = plan_name[:16] + "..."
77
+
78
+ # Check if timeListsFileName exists in current directory
79
+ tl_name = plan.timeListsFileName
80
+ exists = (plan_dir / tl_name).exists() if tl_name else False
81
+ mark = CHECK if exists else CROSS
82
+
83
+ click.echo(f"{stem:<30} {plan_name:<20} {mark}{tl_name:<30}")
@@ -0,0 +1,86 @@
1
+ """
2
+ CLI command for running retirement planning cases.
3
+
4
+ This module provides the 'run' command for executing retirement planning
5
+ optimization from the command line.
6
+
7
+ Copyright (C) 2025-2026 The Owlplanner Authors
8
+
9
+ This program is free software: you can redistribute it and/or modify
10
+ it under the terms of the GNU General Public License as published by
11
+ the Free Software Foundation, either version 3 of the License, or
12
+ (at your option) any later version.
13
+
14
+ This program is distributed in the hope that it will be useful,
15
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
16
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
+ GNU General Public License for more details.
18
+
19
+ You should have received a copy of the GNU General Public License
20
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
21
+ """
22
+
23
+ import click
24
+ from loguru import logger
25
+ import owlplanner as owl
26
+ from pathlib import Path
27
+
28
+
29
+ def validate_toml(ctx, param, value: Path):
30
+ if value is None:
31
+ return None
32
+
33
+ # If no suffix, append .toml
34
+ if value.suffix == "":
35
+ value = value.with_suffix(".toml")
36
+
37
+ # Enforce .toml extension
38
+ if value.suffix.lower() != ".toml":
39
+ raise click.BadParameter("File must have a .toml extension")
40
+
41
+ # Check existence AFTER normalization
42
+ if not value.exists():
43
+ raise click.BadParameter(f"File '{value}' does not exist")
44
+
45
+ if not value.is_file():
46
+ raise click.BadParameter(f"'{value}' is not a file")
47
+
48
+ return value
49
+
50
+
51
+ @click.command(name="run")
52
+ @click.argument(
53
+ "filename",
54
+ type=click.Path(exists=False, dir_okay=False, path_type=Path),
55
+ callback=validate_toml,
56
+ )
57
+ @click.option(
58
+ "--with-config",
59
+ "with_config",
60
+ type=click.Choice(["no", "first", "last"], case_sensitive=False),
61
+ default="first",
62
+ show_default=True,
63
+ help="Include config TOML sheet at the first or last position.",
64
+ )
65
+ def cmd_run(filename: Path, with_config: str):
66
+ """Run the solver for an input OWL plan file.
67
+
68
+ FILENAME is the OWL plan file to run. If no extension is provided,
69
+ .toml will be appended. The file must exist.
70
+
71
+ An output Excel file with results will be created in the current directory.
72
+ The output filename is derived from the input filename by appending
73
+ '_results.xlsx' to the stem of the input filename.
74
+
75
+ Optionally include the case configuration as a TOML worksheet.
76
+
77
+ """
78
+ logger.debug(f"Executing the run command with file: {filename}")
79
+
80
+ plan = owl.readConfig(str(filename), logstreams="loguru", readContributions=True)
81
+ plan.solve(plan.objective, plan.solverOptions)
82
+ click.echo(f"Case status: {plan.caseStatus}")
83
+ if plan.caseStatus == "solved":
84
+ output_filename = filename.with_name(filename.stem + "_results.xlsx")
85
+ plan.saveWorkbook(basename=output_filename, overwrite=True, with_config=with_config)
86
+ click.echo(f"Results saved to: {output_filename}")