owlplanner 2025.12.20__py3-none-any.whl → 2026.2.2__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.
- owlplanner/__init__.py +20 -1
- owlplanner/abcapi.py +18 -17
- owlplanner/cli/README.md +50 -0
- owlplanner/cli/_main.py +52 -0
- owlplanner/cli/cli_logging.py +56 -0
- owlplanner/cli/cmd_list.py +83 -0
- owlplanner/cli/cmd_run.py +86 -0
- owlplanner/config.py +315 -118
- owlplanner/data/__init__.py +21 -0
- owlplanner/data/rates.csv +99 -98
- owlplanner/debts.py +36 -8
- owlplanner/fixedassets.py +95 -21
- owlplanner/mylogging.py +157 -25
- owlplanner/plan.py +938 -390
- owlplanner/plotting/__init__.py +16 -3
- owlplanner/plotting/base.py +17 -3
- owlplanner/plotting/factory.py +16 -3
- owlplanner/plotting/matplotlib_backend.py +30 -7
- owlplanner/plotting/plotly_backend.py +32 -9
- owlplanner/progress.py +16 -3
- owlplanner/rates.py +50 -34
- owlplanner/socialsecurity.py +28 -19
- owlplanner/tax2026.py +119 -38
- owlplanner/timelists.py +194 -18
- owlplanner/utils.py +179 -4
- owlplanner/version.py +20 -1
- {owlplanner-2025.12.20.dist-info → owlplanner-2026.2.2.dist-info}/METADATA +11 -3
- owlplanner-2026.2.2.dist-info/RECORD +35 -0
- owlplanner-2026.2.2.dist-info/entry_points.txt +2 -0
- owlplanner-2026.2.2.dist-info/licenses/AUTHORS +15 -0
- owlplanner/tax2025.py +0 -359
- owlplanner-2025.12.20.dist-info/RECORD +0 -29
- {owlplanner-2025.12.20.dist-info → owlplanner-2026.2.2.dist-info}/WHEEL +0 -0
- {owlplanner-2025.12.20.dist-info → owlplanner-2026.2.2.dist-info}/licenses/LICENSE +0 -0
owlplanner/mylogging.py
CHANGED
|
@@ -1,22 +1,64 @@
|
|
|
1
1
|
"""
|
|
2
|
+
Logging utility module with support for multiple backends.
|
|
2
3
|
|
|
3
|
-
|
|
4
|
+
This module provides a flexible logging system that supports both standard
|
|
5
|
+
Python logging and loguru backends, with verbose mode control and stream management.
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
Copyright (C) 2025-2026 The Owlplanner Authors
|
|
6
8
|
|
|
7
|
-
|
|
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.
|
|
8
13
|
|
|
9
|
-
|
|
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.
|
|
10
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/>.
|
|
11
21
|
"""
|
|
12
22
|
|
|
13
23
|
import sys
|
|
24
|
+
import copy
|
|
25
|
+
import inspect
|
|
26
|
+
import os
|
|
27
|
+
|
|
28
|
+
# Conditional import of loguru - only available if package is installed
|
|
29
|
+
try:
|
|
30
|
+
from loguru import logger as loguru_logger
|
|
31
|
+
HAS_LOGURU = True
|
|
32
|
+
except ImportError:
|
|
33
|
+
loguru_logger = None
|
|
34
|
+
HAS_LOGURU = False
|
|
14
35
|
|
|
15
36
|
|
|
16
37
|
class Logger(object):
|
|
17
38
|
def __init__(self, verbose=True, logstreams=None):
|
|
18
39
|
self._verbose = verbose
|
|
19
40
|
self._prevState = self._verbose
|
|
41
|
+
self._verboseStack = [] # Stack to track verbose states for proper restoration
|
|
42
|
+
self._use_loguru = False
|
|
43
|
+
|
|
44
|
+
# --- Detect loguru backend ---------------------------------
|
|
45
|
+
if logstreams == "loguru" or logstreams == ["loguru"]:
|
|
46
|
+
if not HAS_LOGURU:
|
|
47
|
+
raise ImportError(
|
|
48
|
+
"loguru is required when using loguru logging backend. "
|
|
49
|
+
"Install it with: pip install loguru"
|
|
50
|
+
)
|
|
51
|
+
self._use_loguru = True
|
|
52
|
+
self._logstreams = None
|
|
53
|
+
|
|
54
|
+
loguru_logger.debug("Using loguru as logging backend.")
|
|
55
|
+
return
|
|
56
|
+
|
|
57
|
+
# --- Existing stream-based behavior ------------------------
|
|
58
|
+
# First check if logstreams is a valid type (list or None)
|
|
59
|
+
if logstreams is not None and not isinstance(logstreams, list):
|
|
60
|
+
raise ValueError(f"Log streams {logstreams} must be a list.")
|
|
61
|
+
|
|
20
62
|
if logstreams is None or logstreams == [] or len(logstreams) > 2:
|
|
21
63
|
self._logstreams = [sys.stdout, sys.stderr]
|
|
22
64
|
self.vprint("Using stdout and stderr as stream loggers.")
|
|
@@ -26,59 +68,149 @@ class Logger(object):
|
|
|
26
68
|
elif len(logstreams) == 1:
|
|
27
69
|
self._logstreams = 2 * logstreams
|
|
28
70
|
self.vprint("Using logstream as stream logger.")
|
|
29
|
-
else:
|
|
30
|
-
raise ValueError(f"Log streams {logstreams} must be a list.")
|
|
31
71
|
|
|
32
72
|
def setVerbose(self, verbose=True):
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
or False to make it silent.
|
|
36
|
-
"""
|
|
73
|
+
# Push current state onto stack before changing it
|
|
74
|
+
self._verboseStack.append(self._verbose)
|
|
37
75
|
self._prevState = self._verbose
|
|
38
76
|
self._verbose = verbose
|
|
39
77
|
self.vprint("Setting verbose to", verbose)
|
|
40
|
-
|
|
41
78
|
return self._prevState
|
|
42
79
|
|
|
43
80
|
def resetVerbose(self):
|
|
81
|
+
# Pop the previous state from the stack if available
|
|
82
|
+
if self._verboseStack:
|
|
83
|
+
self._verbose = self._verboseStack.pop()
|
|
84
|
+
self._prevState = self._verbose
|
|
85
|
+
else:
|
|
86
|
+
# Fallback to _prevState if stack is empty (shouldn't happen in normal usage)
|
|
87
|
+
self._verbose = self._prevState
|
|
88
|
+
|
|
89
|
+
def __deepcopy__(self, memo):
|
|
44
90
|
"""
|
|
45
|
-
|
|
91
|
+
Custom deepcopy implementation to handle file descriptors properly.
|
|
92
|
+
Creates a new Logger instance with the same settings instead of
|
|
93
|
+
attempting to copy file descriptors (sys.stdout, sys.stderr, etc.).
|
|
46
94
|
"""
|
|
47
|
-
|
|
95
|
+
# Determine logstreams parameter for new instance
|
|
96
|
+
if self._use_loguru:
|
|
97
|
+
logstreams = "loguru"
|
|
98
|
+
elif self._logstreams is None:
|
|
99
|
+
logstreams = None
|
|
100
|
+
elif self._logstreams == [sys.stdout, sys.stderr]:
|
|
101
|
+
# Default case - will be recreated as [sys.stdout, sys.stderr]
|
|
102
|
+
logstreams = None
|
|
103
|
+
else:
|
|
104
|
+
# Custom streams - preserve them (they might be StringIO or similar)
|
|
105
|
+
logstreams = self._logstreams
|
|
106
|
+
|
|
107
|
+
# Create a new Logger instance with the same settings
|
|
108
|
+
new_logger = Logger(
|
|
109
|
+
verbose=self._verbose,
|
|
110
|
+
logstreams=logstreams
|
|
111
|
+
)
|
|
48
112
|
|
|
49
|
-
|
|
113
|
+
# Copy the verbose stack state
|
|
114
|
+
new_logger._verboseStack = copy.deepcopy(self._verboseStack, memo)
|
|
115
|
+
new_logger._prevState = self._prevState
|
|
116
|
+
|
|
117
|
+
return new_logger
|
|
118
|
+
|
|
119
|
+
# ------------------------------------------------------------
|
|
120
|
+
# Printing methods
|
|
121
|
+
# ------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def print(self, *args, tag="INFO", **kwargs):
|
|
50
124
|
"""
|
|
51
|
-
Unconditional printing regardless of
|
|
52
|
-
previously set.
|
|
125
|
+
Unconditional printing regardless of verbosity.
|
|
53
126
|
"""
|
|
127
|
+
|
|
128
|
+
if self._use_loguru:
|
|
129
|
+
loguru_logger.opt(depth=1).debug(" ".join(map(str, args)))
|
|
130
|
+
return
|
|
131
|
+
|
|
132
|
+
# Get caller information (loguru style: name:function:line)
|
|
133
|
+
frame = inspect.currentframe()
|
|
134
|
+
caller_frame = frame.f_back
|
|
135
|
+
filename = os.path.basename(caller_frame.f_code.co_filename)
|
|
136
|
+
# Remove .py extension if present
|
|
137
|
+
if filename.endswith('.py'):
|
|
138
|
+
filename = filename[:-3]
|
|
139
|
+
function_name = caller_frame.f_code.co_name
|
|
140
|
+
line_number = caller_frame.f_lineno
|
|
141
|
+
location = f"{filename}:{function_name}:{line_number}"
|
|
142
|
+
|
|
143
|
+
# Format message with timestamp, location, and tag
|
|
144
|
+
from datetime import datetime
|
|
145
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
146
|
+
message = " ".join(map(str, args))
|
|
147
|
+
formatted_message = f"{timestamp} | {tag} | {location} | {message}"
|
|
148
|
+
|
|
54
149
|
if "file" not in kwargs:
|
|
55
150
|
file = self._logstreams[0]
|
|
56
151
|
kwargs["file"] = file
|
|
152
|
+
else:
|
|
153
|
+
file = kwargs["file"]
|
|
57
154
|
|
|
58
|
-
print(
|
|
155
|
+
print(formatted_message, **kwargs)
|
|
59
156
|
file.flush()
|
|
60
157
|
|
|
61
|
-
def vprint(self, *args, **kwargs):
|
|
158
|
+
def vprint(self, *args, tag="DEBUG", **kwargs):
|
|
62
159
|
"""
|
|
63
|
-
Conditional printing depending on
|
|
64
|
-
previously set.
|
|
160
|
+
Conditional printing depending on verbose flag.
|
|
65
161
|
"""
|
|
66
162
|
if self._verbose:
|
|
67
|
-
self.
|
|
163
|
+
if self._use_loguru:
|
|
164
|
+
loguru_logger.opt(depth=1).debug(" ".join(map(str, args)))
|
|
165
|
+
return
|
|
166
|
+
|
|
167
|
+
# Get caller information (loguru style: name:function:line)
|
|
168
|
+
frame = inspect.currentframe()
|
|
169
|
+
caller_frame = frame.f_back
|
|
170
|
+
filename = os.path.basename(caller_frame.f_code.co_filename)
|
|
171
|
+
# Remove .py extension if present
|
|
172
|
+
if filename.endswith('.py'):
|
|
173
|
+
filename = filename[:-3]
|
|
174
|
+
function_name = caller_frame.f_code.co_name
|
|
175
|
+
line_number = caller_frame.f_lineno
|
|
176
|
+
location = f"{filename}:{function_name}:{line_number}"
|
|
177
|
+
|
|
178
|
+
# Format message with timestamp, location, and tag
|
|
179
|
+
from datetime import datetime
|
|
180
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
181
|
+
message = " ".join(map(str, args))
|
|
182
|
+
formatted_message = f"{timestamp} | {tag} | {location} | {message}"
|
|
183
|
+
|
|
184
|
+
if "file" not in kwargs:
|
|
185
|
+
file = self._logstreams[0]
|
|
186
|
+
kwargs["file"] = file
|
|
187
|
+
else:
|
|
188
|
+
file = kwargs["file"]
|
|
189
|
+
|
|
190
|
+
print(formatted_message, **kwargs)
|
|
191
|
+
file.flush()
|
|
68
192
|
|
|
69
193
|
def xprint(self, *args, **kwargs):
|
|
70
194
|
"""
|
|
71
|
-
Print message and exit.
|
|
72
|
-
The exit() used throws an exception in an interactive environment.
|
|
195
|
+
Print message and exit. Used for fatal errors.
|
|
73
196
|
"""
|
|
197
|
+
if self._use_loguru:
|
|
198
|
+
loguru_logger.opt(depth=1).debug("ERROR: " + " ".join(map(str, args)))
|
|
199
|
+
loguru_logger.opt(depth=1).debug("Exiting...")
|
|
200
|
+
raise Exception("Fatal error.")
|
|
201
|
+
|
|
74
202
|
if "file" not in kwargs:
|
|
75
203
|
file = self._logstreams[1]
|
|
76
204
|
kwargs["file"] = file
|
|
205
|
+
else:
|
|
206
|
+
file = kwargs["file"]
|
|
77
207
|
|
|
78
208
|
if self._verbose:
|
|
79
209
|
print("ERROR:", *args, **kwargs)
|
|
80
|
-
print("Exiting...")
|
|
210
|
+
print("Exiting...", file=file)
|
|
81
211
|
file.flush()
|
|
82
212
|
|
|
83
213
|
raise Exception("Fatal error.")
|
|
84
|
-
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# Log filtering utility functions removed - no longer needed since StringIO guarantees ordered messages
|