owlplanner 2025.5.5__py3-none-any.whl → 2025.5.15__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.15"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: owlplanner
3
- Version: 2025.5.5
3
+ Version: 2025.5.15
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
@@ -693,8 +693,11 @@ Classifier: Topic :: Office/Business :: Financial :: Investment
693
693
  Requires-Python: >=3.8
694
694
  Requires-Dist: matplotlib
695
695
  Requires-Dist: numpy
696
+ Requires-Dist: odfpy
696
697
  Requires-Dist: openpyxl
697
698
  Requires-Dist: pandas
699
+ Requires-Dist: plotly
700
+ Requires-Dist: pulp
698
701
  Requires-Dist: scipy
699
702
  Requires-Dist: seaborn
700
703
  Requires-Dist: streamlit
@@ -711,15 +714,17 @@ Description-Content-Type: text/markdown
711
714
  -------------------------------------------------------------------------------------
712
715
 
713
716
  ### 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.
717
+ Owl is a financial retirement planning tool that uses a linear programming
718
+ optimization algorithm to provide guidance on retirement decisions
719
+ such as contributions, withdrawals, Roth conversions, and more.
716
720
  Users can select varying return rates to perform historical back testing,
717
721
  stochastic rates for performing Monte Carlo analyses,
718
722
  or fixed rates either derived from historical averages, or set by the user.
719
723
 
720
724
  There are a few ways to run Owl:
721
725
 
722
- - Run Owl directly on the Streamlit Community Server at [owlplanner.streamlit.app](https://owlplanner.streamlit.app).
726
+ - Run Owl directly on the Streamlit Community Server at
727
+ [owlplanner.streamlit.app](https://owlplanner.streamlit.app).
723
728
 
724
729
  - Run locally on your computer using a Docker image.
725
730
  Follow these [instructions](docker/README.md) for this option.
@@ -729,7 +734,7 @@ Follow these [instructions](INSTALL.md) to install Owl from the source code and
729
734
 
730
735
  -------------------------------------------------------------------------------------
731
736
  ## Overview
732
- This package is a retirement modeling framework for exploring the sensitivity of retirement financial decisions.
737
+ This package is a modeling framework for exploring the sensitivity of retirement financial decisions.
733
738
  Strictly speaking, it is not a planning tool, but more an environment for exploring *what if* scenarios.
734
739
  It provides different realizations of a financial strategy through the rigorous
735
740
  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=qgj6Tm2TUGY2hnWl84UnqBg_PfKL41UcaRHV7ghcCOI,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=mu6V1pH-jOkKVs2QfQVQ_nlYgrniiHk4nFCx_ygJhiE,33036
19
+ owlplanner-2025.5.15.dist-info/METADATA,sha256=kw_BIk15vSKoVPiBCUMl40dVrVPrtoweNhqvIZZP52o,54024
20
+ owlplanner-2025.5.15.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
+ owlplanner-2025.5.15.dist-info/licenses/LICENSE,sha256=IwGE9guuL-ryRPEKi6wFPI_zOhg7zDZbTYuHbSt_SAk,35823
22
+ owlplanner-2025.5.15.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