owlplanner 2025.5.5__py3-none-any.whl → 2025.5.12__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/rates.py CHANGED
@@ -35,9 +35,8 @@ import os
35
35
  import sys
36
36
  from datetime import date
37
37
 
38
- from owlplanner import logging
38
+ from owlplanner import mylogging as log
39
39
  from owlplanner import utils as u
40
- from owlplanner import plots
41
40
 
42
41
  # All data goes from 1928 to 2024. Update the TO value when data
43
42
  # becomes available for subsequent years.
@@ -78,7 +77,7 @@ def getRatesDistributions(frm, to, mylog=None):
78
77
  the different rates. Function returns means and covariance matrix.
79
78
  """
80
79
  if mylog is None:
81
- mylog = logging.Logger()
80
+ mylog = log.Logger()
82
81
 
83
82
  # Convert years to index and check range.
84
83
  frm -= FROM
@@ -160,7 +159,7 @@ class Rates(object):
160
159
  Default constructor.
161
160
  """
162
161
  if mylog is None:
163
- self.mylog = logging.Logger()
162
+ self.mylog = log.Logger()
164
163
  else:
165
164
  self.mylog = mylog
166
165
 
@@ -383,13 +382,3 @@ class Rates(object):
383
382
  srates = np.random.multivariate_normal(self.means, self.covar)
384
383
 
385
384
  return srates
386
-
387
-
388
- def showRatesDistributions(frm=FROM, to=TO):
389
- """
390
- Plot histograms of the rates distributions.
391
- """
392
- fig = plots.show_rates_distributions(frm, to, SP500, BondsBaa, TNotes, Inflation, FROM)
393
- return fig
394
- # plt.show()
395
- # return None
owlplanner/timelists.py CHANGED
@@ -83,7 +83,7 @@ def condition(dfDict, inames, horizons, mylog):
83
83
  df = df.loc[:, ~df.columns.str.contains("^Unnamed")]
84
84
  for col in df.columns:
85
85
  if col == "" or col not in timeHorizonItems:
86
- df.drop(col, axis=1)
86
+ df.drop(col, axis=1, inplace=True)
87
87
 
88
88
  for item in timeHorizonItems:
89
89
  if item not in df.columns:
@@ -114,12 +114,7 @@ def condition(dfDict, inames, horizons, mylog):
114
114
 
115
115
  if df["year"].iloc[-1] != endyear - 1:
116
116
  raise ValueError(
117
- "Time horizon for",
118
- iname,
119
- "is too short.\n\tIt should end in",
120
- endyear,
121
- "but ends in",
122
- df["year"].iloc[-1],
117
+ f"Time horizon for {iname} too short.\n\tIt should end in {endyear}, not {df['year'].iloc[-1]}"
123
118
  )
124
119
 
125
120
  return timeLists
owlplanner/version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "2025.05.05"
1
+ __version__ = "2025.05.12"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.5.5
3
+ Version: 2025.5.12
4
4
  Summary: Owl: Retirement planner with great wisdom
5
5
  Project-URL: HomePage, https://github.com/mdlacasse/owl
6
6
  Project-URL: Repository, https://github.com/mdlacasse/owl
@@ -695,6 +695,7 @@ Requires-Dist: matplotlib
695
695
  Requires-Dist: numpy
696
696
  Requires-Dist: openpyxl
697
697
  Requires-Dist: pandas
698
+ Requires-Dist: plotly
698
699
  Requires-Dist: scipy
699
700
  Requires-Dist: seaborn
700
701
  Requires-Dist: streamlit
@@ -711,15 +712,17 @@ Description-Content-Type: text/markdown
711
712
  -------------------------------------------------------------------------------------
712
713
 
713
714
  ### TL;DR
714
- Owl is a retirement planning tool that uses a linear programming optimization algorithm
715
- to provide guidance on retirement decisions, including Roth conversions.
715
+ Owl is a financial retirement planning tool that uses a linear programming
716
+ optimization algorithm to provide guidance on retirement decisions
717
+ such as contributions, withdrawals, Roth conversions, and more.
716
718
  Users can select varying return rates to perform historical back testing,
717
719
  stochastic rates for performing Monte Carlo analyses,
718
720
  or fixed rates either derived from historical averages, or set by the user.
719
721
 
720
722
  There are a few ways to run Owl:
721
723
 
722
- - Run Owl directly on the Streamlit Community Server at [owlplanner.streamlit.app](https://owlplanner.streamlit.app).
724
+ - Run Owl directly on the Streamlit Community Server at
725
+ [owlplanner.streamlit.app](https://owlplanner.streamlit.app).
723
726
 
724
727
  - Run locally on your computer using a Docker image.
725
728
  Follow these [instructions](docker/README.md) for this option.
@@ -729,7 +732,7 @@ Follow these [instructions](INSTALL.md) to install Owl from the source code and
729
732
 
730
733
  -------------------------------------------------------------------------------------
731
734
  ## Overview
732
- This package is a retirement modeling framework for exploring the sensitivity of retirement financial decisions.
735
+ This package is a modeling framework for exploring the sensitivity of retirement financial decisions.
733
736
  Strictly speaking, it is not a planning tool, but more an environment for exploring *what if* scenarios.
734
737
  It provides different realizations of a financial strategy through the rigorous
735
738
  mathematical optimization of relevant decision variables. Two major objective goals can be set: either
@@ -0,0 +1,22 @@
1
+ owlplanner/__init__.py,sha256=hJ2i4m2JpHPAKyQLjYOXpJzeEsgcTcKD-Vhm0AIjjWg,592
2
+ owlplanner/abcapi.py,sha256=m0vtoEzz9HJV7fOK_d7OnK7ha2Qbf7wLLPCJ9YZzR1k,6851
3
+ owlplanner/config.py,sha256=qIWzj3Tbz_jhhFqIkaMzXzgWQBN4Uk2km_VIMZSh910,12559
4
+ owlplanner/mylogging.py,sha256=tYMw04O-XYSzjTj36fmKJGLcE1VkK6k6oJNeqtKXzuc,2530
5
+ owlplanner/plan.py,sha256=_Oq2UomWzteYVlT6zJC_A2zXO3YVlXG9xgUMcQfWBJg,106742
6
+ owlplanner/progress.py,sha256=8jlCvvtgDI89zXVNMBg1-lnEyhpPvKQS2X5oAIpoOVQ,384
7
+ owlplanner/rates.py,sha256=MiaibxJY82JGpAhGyF2BJTm5-rmVAUuG8KLApVQhjvU,14816
8
+ owlplanner/tax2025.py,sha256=wmlZpYeeGNrbyn5g7wOFqhWbggppodtHqc-ex5XRooI,7850
9
+ owlplanner/timelists.py,sha256=wNYnJqxJ6QqE6jHh5lfFqYngfw5wUFrI15LSsM5ae8s,3949
10
+ owlplanner/utils.py,sha256=WpJgn79YZfH8UCkcmhd-AZlxlGuz1i1-UDBRXImsY6I,2485
11
+ owlplanner/version.py,sha256=5ZqN4YDTyjz76bfj2w5OuF_20x5Fau01NRLJ8nXtxx8,28
12
+ owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
+ owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
14
+ owlplanner/plotting/__init__.py,sha256=VnF6ui78YrTrg1dA6hBIdI02ahzEaHVR3ZEdDe_i880,103
15
+ owlplanner/plotting/base.py,sha256=1bU6iM1pGIE-l9p0GuulX4gRK_7ds96784Wb5oVUUR0,2449
16
+ owlplanner/plotting/factory.py,sha256=i1k8m_ISnJw06f_JWlMvOQ7Q0PgV_BoLm05uLwFPvOQ,883
17
+ owlplanner/plotting/matplotlib_backend.py,sha256=-M4Am7N0D8Nfv_tKNA1TneFYU_DuW_ZsoUBHRQWD_ok,17887
18
+ owlplanner/plotting/plotly_backend.py,sha256=LUmeWYAA_F1vdl-GPehzprVx3oFWehGmc1Oir-kNJ_w,33093
19
+ owlplanner-2025.5.12.dist-info/METADATA,sha256=w0R8yDGSwrRGCouu2Hbe0Cx4tvQInuwm6VcvHcjFN9Q,53983
20
+ owlplanner-2025.5.12.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
+ owlplanner-2025.5.12.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
22
+ owlplanner-2025.5.12.dist-info/RECORD,,
owlplanner/plots.py DELETED
@@ -1,296 +0,0 @@
1
- """
2
-
3
- Owl/plots
4
- ---------
5
-
6
- A retirement planner using linear programming optimization.
7
-
8
- This module contains all plotting functions used by the Owl project.
9
-
10
- Copyright (C) 2025 -- Martin-D. Lacasse
11
-
12
- Disclaimer: This program comes with no guarantee. Use at your own risk.
13
- """
14
-
15
- import numpy as np
16
- import pandas as pd
17
- import matplotlib.pyplot as plt
18
- import matplotlib.ticker as tk
19
- import io
20
- import os
21
-
22
- os.environ["JUPYTER_PLATFORM_DIRS"] = "1"
23
- import seaborn as sbn
24
-
25
- from owlplanner import utils as u
26
-
27
-
28
- def line_income_plot(x, series, style, title, yformat="\\$k"):
29
- """
30
- Core line plotter function.
31
- """
32
- fig, ax = plt.subplots(figsize=(6, 4))
33
- plt.grid(visible="both")
34
-
35
- for sname in series:
36
- ax.plot(x, series[sname], label=sname, ls=style[sname])
37
-
38
- ax.legend(loc="upper left", reverse=True, fontsize=8, framealpha=0.3)
39
- ax.set_title(title)
40
- ax.set_xlabel("year")
41
- ax.set_ylabel(yformat)
42
- ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
43
- if "k" in yformat:
44
- ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000), ",")))
45
- # Give range to y values in unindexed flat profiles.
46
- ymin, ymax = ax.get_ylim()
47
- if ymax - ymin < 5000:
48
- ax.set_ylim((ymin * 0.95, ymax * 1.05))
49
-
50
- return fig, ax
51
-
52
-
53
- def stack_plot(x, inames, title, irange, series, snames, location, yformat="\\$k"):
54
- """
55
- Core function for stacked plots.
56
- """
57
- nonzeroSeries = {}
58
- for sname in snames:
59
- for i in irange:
60
- tmp = series[sname][i]
61
- if sum(tmp) > 1.0:
62
- nonzeroSeries[sname + " " + inames[i]] = tmp
63
-
64
- if len(nonzeroSeries) == 0:
65
- return None, None
66
-
67
- fig, ax = plt.subplots(figsize=(6, 4))
68
- plt.grid(visible="both")
69
-
70
- ax.stackplot(x, nonzeroSeries.values(), labels=nonzeroSeries.keys(), alpha=0.6)
71
- ax.legend(loc=location, reverse=True, fontsize=8, ncol=2, framealpha=0.5)
72
- ax.set_title(title)
73
- ax.set_xlabel("year")
74
- ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
75
- if "k" in yformat:
76
- ax.set_ylabel(yformat)
77
- ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(x / 1000), ",")))
78
- elif yformat == "percent":
79
- ax.set_ylabel("%")
80
- ax.get_yaxis().set_major_formatter(tk.FuncFormatter(lambda x, p: format(int(100 * x), ",")))
81
- else:
82
- raise RuntimeError(f"Unknown yformat: {yformat}.")
83
-
84
- return fig, ax
85
-
86
-
87
- def show_histogram_results(objective, df, N, year_n, n_d=None, N_i=1, phi_j=None):
88
- """
89
- Show a histogram of values from historical data or Monte Carlo simulations.
90
- """
91
- description = io.StringIO()
92
-
93
- pSuccess = u.pc(len(df) / N)
94
- print(f"Success rate: {pSuccess} on {N} samples.", file=description)
95
- title = f"$N$ = {N}, $P$ = {pSuccess}"
96
- means = df.mean(axis=0, numeric_only=True)
97
- medians = df.median(axis=0, numeric_only=True)
98
-
99
- my = 2 * [year_n[-1]]
100
- if N_i == 2 and n_d is not None and n_d < len(year_n):
101
- my[0] = year_n[n_d - 1]
102
-
103
- # Don't show partial bequest of zero if spouse is full beneficiary,
104
- # or if solution led to empty accounts at the end of first spouse's life.
105
- if (phi_j is not None and np.all(phi_j == 1)) or medians.iloc[0] < 1:
106
- if medians.iloc[0] < 1:
107
- print(f"Optimized solutions all have null partial bequest in year {my[0]}.", file=description)
108
- df.drop("partial", axis=1, inplace=True)
109
- means = df.mean(axis=0, numeric_only=True)
110
- medians = df.median(axis=0, numeric_only=True)
111
-
112
- df /= 1000
113
- if len(df) > 0:
114
- thisyear = year_n[0]
115
- if objective == "maxBequest":
116
- fig, axes = plt.subplots()
117
- # Show both partial and final bequests in the same histogram.
118
- sbn.histplot(df, multiple="dodge", kde=True, ax=axes)
119
- legend = []
120
- # Don't know why but legend is reversed from df.
121
- for q in range(len(means) - 1, -1, -1):
122
- dmedian = u.d(medians.iloc[q], latex=True)
123
- dmean = u.d(means.iloc[q], latex=True)
124
- legend.append(f"{my[q]}: $M$: {dmedian}, $\\bar{{x}}$: {dmean}")
125
- plt.legend(legend, shadow=True)
126
- plt.xlabel(f"{thisyear} $k")
127
- plt.title(objective)
128
- leads = [f"partial {my[0]}", f" final {my[1]}"]
129
- elif len(means) == 2:
130
- # Show partial bequest and net spending as two separate histograms.
131
- fig, axes = plt.subplots(1, 2, figsize=(10, 5))
132
- cols = ["partial", objective]
133
- leads = [f"partial {my[0]}", objective]
134
- for q in range(2):
135
- sbn.histplot(df[cols[q]], kde=True, ax=axes[q])
136
- dmedian = u.d(medians.iloc[q], latex=True)
137
- dmean = u.d(means.iloc[q], latex=True)
138
- legend = [f"$M$: {dmedian}, $\\bar{{x}}$: {dmean}"]
139
- axes[q].set_label(legend)
140
- axes[q].legend(labels=legend)
141
- axes[q].set_title(leads[q])
142
- axes[q].set_xlabel(f"{thisyear} $k")
143
- else:
144
- # Show net spending as single histogram.
145
- fig, axes = plt.subplots()
146
- sbn.histplot(df[objective], kde=True, ax=axes)
147
- dmedian = u.d(medians.iloc[0], latex=True)
148
- dmean = u.d(means.iloc[0], latex=True)
149
- legend = [f"$M$: {dmedian}, $\\bar{{x}}$: {dmean}"]
150
- plt.legend(legend, shadow=True)
151
- plt.xlabel(f"{thisyear} $k")
152
- plt.title(objective)
153
- leads = [objective]
154
-
155
- plt.suptitle(title)
156
-
157
- for q in range(len(means)):
158
- print(f"{leads[q]:>12}: Median ({thisyear} $): {u.d(medians.iloc[q])}", file=description)
159
- print(f"{leads[q]:>12}: Mean ({thisyear} $): {u.d(means.iloc[q])}", file=description)
160
- mmin = 1000 * df.iloc[:, q].min()
161
- mmax = 1000 * df.iloc[:, q].max()
162
- print(f"{leads[q]:>12}: Range: {u.d(mmin)} - {u.d(mmax)}", file=description)
163
- nzeros = len(df.iloc[:, q][df.iloc[:, q] < 0.001])
164
- print(f"{leads[q]:>12}: N zero solns: {nzeros}", file=description)
165
-
166
- return fig, description
167
-
168
- return None, description
169
-
170
-
171
- def show_rates_correlations(name, tau_kn, N_n, rate_method, rate_frm=None, rate_to=None, tag="", share_range=False):
172
- """
173
- Plot correlations between various rates.
174
- """
175
- rate_names = [
176
- "S&P500 (incl. div.)",
177
- "Baa Corp. Bonds",
178
- "10-y T-Notes",
179
- "Inflation",
180
- ]
181
-
182
- df = pd.DataFrame()
183
- for k, name in enumerate(rate_names):
184
- data = 100 * tau_kn[k]
185
- df[name] = data
186
-
187
- g = sbn.PairGrid(df, diag_sharey=False, height=1.8, aspect=1)
188
- if share_range:
189
- minval = df.min().min() - 5
190
- maxval = df.max().max() + 5
191
- g.set(xlim=(minval, maxval), ylim=(minval, maxval))
192
- g.map_upper(sbn.scatterplot)
193
- g.map_lower(sbn.kdeplot)
194
- g.map_diag(sbn.histplot, color="orange")
195
-
196
- # Put zero axes on off-diagonal plots.
197
- imod = len(rate_names) + 1
198
- for i, ax in enumerate(g.axes.flat):
199
- ax.axvline(x=0, color="grey", linewidth=1, linestyle=":")
200
- if i % imod != 0:
201
- ax.axhline(y=0, color="grey", linewidth=1, linestyle=":")
202
-
203
- title = name + "\n"
204
- title += f"Rates Correlations (N={N_n}) {rate_method}"
205
- if rate_method in ["historical", "histochastic"]:
206
- title += f" ({rate_frm}-{rate_to})"
207
-
208
- if tag != "":
209
- title += " - " + tag
210
-
211
- g.fig.suptitle(title, y=1.08)
212
- return g.fig
213
-
214
-
215
- def show_rates(name, tau_kn, year_n, year_frac_left, N_k, rate_method, rate_frm=None, rate_to=None, tag=""):
216
- """
217
- Plot rate values used over the time horizon.
218
- """
219
- fig, ax = plt.subplots(figsize=(6, 4))
220
- plt.grid(visible="both")
221
- title = name + "\nReturn & Inflation Rates (" + str(rate_method)
222
- if rate_method in ["historical", "histochastic", "historical average"]:
223
- title += f" {rate_frm}-{rate_to}"
224
- title += ")"
225
-
226
- if tag != "":
227
- title += " - " + tag
228
-
229
- rate_name = [
230
- "S&P500 (incl. div.)",
231
- "Baa Corp. Bonds",
232
- "10-y T-Notes",
233
- "Inflation",
234
- ]
235
- ltype = ["-", "-.", ":", "--"]
236
- for k in range(N_k):
237
- if year_frac_left == 1:
238
- data = 100 * tau_kn[k]
239
- years = year_n
240
- else:
241
- data = 100 * tau_kn[k, 1:]
242
- years = year_n[1:]
243
-
244
- # Use ddof=1 to match pandas.
245
- label = (
246
- rate_name[k] + " <" + "{:.1f}".format(np.mean(data)) + " +/- {:.1f}".format(np.std(data, ddof=1)) + "%>"
247
- )
248
- ax.plot(years, data, label=label, ls=ltype[k % N_k])
249
-
250
- ax.xaxis.set_major_locator(tk.MaxNLocator(integer=True))
251
- ax.legend(loc="best", reverse=False, fontsize=8, framealpha=0.7)
252
- ax.set_title(title)
253
- ax.set_xlabel("year")
254
- ax.set_ylabel("%")
255
- return fig
256
-
257
-
258
- def show_rates_distributions(frm, to, SP500, BondsBaa, TNotes, Inflation, FROM):
259
- """
260
- Plot histograms of the rates distributions.
261
- """
262
- title = f"Rates from {frm} to {to}"
263
- # Bring year values to indices.
264
- frm -= FROM
265
- to -= FROM
266
-
267
- nbins = int((to - frm) / 4)
268
- fig, ax = plt.subplots(1, 4, sharey=True, sharex=True, tight_layout=True)
269
-
270
- dat0 = np.array(SP500[frm:to])
271
- dat1 = np.array(BondsBaa[frm:to])
272
- dat2 = np.array(TNotes[frm:to])
273
- dat3 = np.array(Inflation[frm:to])
274
-
275
- fig.suptitle(title)
276
- ax[0].set_title("S&P500")
277
- label = "<>: " + u.pc(np.mean(dat0), 2, 1)
278
- ax[0].hist(dat0, bins=nbins, label=label)
279
- ax[0].legend(loc="upper left", fontsize=8, framealpha=0.7)
280
-
281
- ax[1].set_title("BondsBaa")
282
- label = "<>: " + u.pc(np.mean(dat1), 2, 1)
283
- ax[1].hist(dat1, bins=nbins, label=label)
284
- ax[1].legend(loc="upper left", fontsize=8, framealpha=0.7)
285
-
286
- ax[2].set_title("TNotes")
287
- label = "<>: " + u.pc(np.mean(dat2), 2, 1)
288
- ax[2].hist(dat2, bins=nbins, label=label)
289
- ax[2].legend(loc="upper left", fontsize=8, framealpha=0.7)
290
-
291
- ax[3].set_title("Inflation")
292
- label = "<>: " + u.pc(np.mean(dat3), 2, 1)
293
- ax[3].hist(dat3, bins=nbins, label=label)
294
- ax[3].legend(loc="upper left", fontsize=8, framealpha=0.7)
295
-
296
- return fig
@@ -1,18 +0,0 @@
1
- owlplanner/__init__.py,sha256=QJgqS0MrRlBUFuI3PY2LbtD-Xxk1keNnbbPmSsdyZL0,552
2
- owlplanner/abcapi.py,sha256=m0vtoEzz9HJV7fOK_d7OnK7ha2Qbf7wLLPCJ9YZzR1k,6851
3
- owlplanner/config.py,sha256=DP-E8EPhkWvMmgPKp26rIrcppkITQyedgLfEyFrySpg,12554
4
- owlplanner/logging.py,sha256=tYMw04O-XYSzjTj36fmKJGLcE1VkK6k6oJNeqtKXzuc,2530
5
- owlplanner/plan.py,sha256=9mWj7AXXSTTfaa9bqqUqjPFuiPYf3HtCH31c0Y7Sryg,110615
6
- owlplanner/plots.py,sha256=0bvKzvPFfUSymWvMV2gGXnQy3-LpjcywYrfJ2-W_8M0,10476
7
- owlplanner/progress.py,sha256=8jlCvvtgDI89zXVNMBg1-lnEyhpPvKQS2X5oAIpoOVQ,384
8
- owlplanner/rates.py,sha256=ct-CmRiuxUxslkoBy14j5p19_FesldQiPvMgw470FKE,15108
9
- owlplanner/tax2025.py,sha256=wmlZpYeeGNrbyn5g7wOFqhWbggppodtHqc-ex5XRooI,7850
10
- owlplanner/timelists.py,sha256=6eqdnpwmNje9sNw1Hy9Gd-2Wcpgjor1TH4sVnFrpLo4,4033
11
- owlplanner/utils.py,sha256=WpJgn79YZfH8UCkcmhd-AZlxlGuz1i1-UDBRXImsY6I,2485
12
- owlplanner/version.py,sha256=suJjWyGl2pv1HKBSdqeb6B361ikXPlXHjEEfihKlKrk,28
13
- owlplanner/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
- owlplanner/data/rates.csv,sha256=6fxg56BVVORrj9wJlUGFdGXKvOX5r7CSca8uhUbbuIU,3734
15
- owlplanner-2025.5.5.dist-info/METADATA,sha256=0QKQkzgOLIwhD32o7WFrIACVzPf15TXHke0uEU--dn0,53926
16
- owlplanner-2025.5.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
17
- owlplanner-2025.5.5.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
18
- owlplanner-2025.5.5.dist-info/RECORD,,
File without changes