glidepath 1.0.2__tar.gz → 1.1.1__tar.gz
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.
- {glidepath-1.0.2 → glidepath-1.1.1}/LICENSE-DATA +5 -2
- {glidepath-1.0.2 → glidepath-1.1.1}/PKG-INFO +43 -6
- {glidepath-1.0.2 → glidepath-1.1.1}/README.md +41 -5
- {glidepath-1.0.2 → glidepath-1.1.1}/pyproject.toml +34 -2
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/files.py +1 -1
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/retirement.py +30 -20
- glidepath-1.1.1/src/glidepath/gui/assets/glidepath.ico +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/forms.py +6 -3
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/widgets.py +18 -17
- {glidepath-1.0.2 → glidepath-1.1.1}/LICENSE +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/__init__.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/__init__.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/backtest.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/charts.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/copy.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/display.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/drawdown.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/example.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/exports.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/firstrun.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/forms.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/inspector.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/labels.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/montecarlo.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/outlook.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/plan.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/retirement.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/scenarios.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/shell.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/app/tables.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/__init__.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/annuities.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/backtest.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/comparison.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/config.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/contributions.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/engine.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/entities.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/glide.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/investments.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/money.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/montecarlo.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/pensions.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/periods.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/provenance.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/randomness.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/region.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/reporting.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/results.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/returns.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/scenarios.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/state_pension.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/tax.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/withdrawals.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/core/wrappers.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/__init__.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/assets/icon_128.png +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/assets/icon_16.png +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/assets/icon_24.png +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/assets/icon_256.png +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/assets/icon_32.png +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/assets/icon_48.png +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/assets/icon_64.png +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/assets/wordmark.png +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/charts.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/inspector.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/main.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/scenarios.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/style.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/gui/tableview.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/persistence/__init__.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/persistence/assumptions.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/persistence/decode.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/persistence/document.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/persistence/encode.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/persistence/migrations.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/persistence/values.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/py.typed +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/regions/__init__.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/regions/uk/__init__.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/regions/uk/ages.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/regions/uk/contributions.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/regions/uk/data/age_rules.toml +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/regions/uk/data/assumptions_default.toml +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/regions/uk/data/returns_history.toml +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/regions/uk/data/tax_year_2026_27.toml +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/regions/uk/extension.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/regions/uk/loader.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/regions/uk/region.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/regions/uk/schema.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/regions/uk/state_pension.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/regions/uk/tax.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/regions/uk/wrappers.py +0 -0
- {glidepath-1.0.2 → glidepath-1.1.1}/src/glidepath/regions/uk/years.py +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Data licence notice
|
|
2
2
|
===================
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Glidepath's own code is MIT-licensed (see LICENSE). The following bundled
|
|
5
|
+
data file has a separate licence:
|
|
6
6
|
|
|
7
7
|
src/glidepath/regions/uk/data/returns_history.toml
|
|
8
8
|
|
|
@@ -26,3 +26,6 @@ The MIT licence grant does not extend to that file. To obtain a
|
|
|
26
26
|
distribution free of CC BY-NC-SA material, delete the file and
|
|
27
27
|
regenerate a replacement from a source of your choosing with
|
|
28
28
|
scripts/build_returns_history.py as a starting point.
|
|
29
|
+
|
|
30
|
+
Third-party runtime licences and notices in third-party-licences/ retain
|
|
31
|
+
their original terms; Glidepath's MIT licence does not replace them.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: glidepath
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.1.1
|
|
4
4
|
Summary: Desktop retirement and investment planner (UK-first, region-extensible).
|
|
5
5
|
Keywords: retirement,pension,financial-planning,investment,uk,sipp,isa,monte-carlo
|
|
6
6
|
Author: James Williams
|
|
@@ -24,6 +24,7 @@ Project-URL: Homepage, https://williajm.github.io/glidepath/
|
|
|
24
24
|
Project-URL: Repository, https://github.com/williajm/glidepath
|
|
25
25
|
Project-URL: Changelog, https://github.com/williajm/glidepath/blob/main/CHANGELOG.md
|
|
26
26
|
Project-URL: Issues, https://github.com/williajm/glidepath/issues
|
|
27
|
+
Project-URL: Statistics, https://pyrank.org/package/glidepath/
|
|
27
28
|
Description-Content-Type: text/markdown
|
|
28
29
|
|
|
29
30
|
<!-- Image and badge URLs are absolute so the README renders on PyPI,
|
|
@@ -45,6 +46,7 @@ Description-Content-Type: text/markdown
|
|
|
45
46
|
|
|
46
47
|
<p align="center">
|
|
47
48
|
<a href="https://williajm.github.io/glidepath/"><b>williajm.github.io/glidepath</b></a>
|
|
49
|
+
· <a href="https://pyrank.org/package/glidepath/">PyPI and GitHub stats on PyRank</a>
|
|
48
50
|
</p>
|
|
49
51
|
|
|
50
52
|
A desktop retirement and investment planner. UK-first, built so other
|
|
@@ -132,7 +134,32 @@ based solely on this tool.
|
|
|
132
134
|
|
|
133
135
|
## Install
|
|
134
136
|
|
|
135
|
-
|
|
137
|
+
### Windows download
|
|
138
|
+
|
|
139
|
+
From **v1.1.0**, Windows x64 users can run Glidepath without Python, uv
|
|
140
|
+
or terminal commands:
|
|
141
|
+
|
|
142
|
+
1. Open the [latest GitHub Release](https://github.com/williajm/glidepath/releases/latest).
|
|
143
|
+
2. Under **Assets**, download `glidepath-X.Y.Z-windows-x64.exe`.
|
|
144
|
+
3. Keep it in a folder of your choice and double-click it to launch.
|
|
145
|
+
|
|
146
|
+
This is a runnable app, not an installer; it does not add shortcuts or
|
|
147
|
+
update itself. For a newer version, close Glidepath and download the new
|
|
148
|
+
executable. Your saved `.glidepath.json` plans stay where you put them.
|
|
149
|
+
|
|
150
|
+
The executable is **not yet signed with a Windows publisher certificate**.
|
|
151
|
+
Windows may show “Windows protected your PC” or “unrecognised app”. Check
|
|
152
|
+
that the file came from this repository's release before deciding whether
|
|
153
|
+
to proceed. GitHub build provenance does not remove that warning.
|
|
154
|
+
If the latest release has no `.exe` yet, use the uv installation below.
|
|
155
|
+
|
|
156
|
+
Runtime licences and attribution notices are included in the executable
|
|
157
|
+
and in the release's `glidepath-X.Y.Z-third-party-licences.zip` download.
|
|
158
|
+
That archive also lists the corresponding upstream sources.
|
|
159
|
+
|
|
160
|
+
### Install with uv
|
|
161
|
+
|
|
162
|
+
Alternatively, install Glidepath from [PyPI](https://pypi.org/project/glidepath/)
|
|
136
163
|
with [uv](https://docs.astral.sh/uv/), which also fetches the Python it
|
|
137
164
|
needs — you do not need Python installed first.
|
|
138
165
|
|
|
@@ -238,14 +265,24 @@ Releases are `vX.Y.Z` tags on `main`. Each one is published to
|
|
|
238
265
|
trusted publishing with PEP 740 attestations, and as a GitHub Release
|
|
239
266
|
carrying its notes from
|
|
240
267
|
[`CHANGELOG.md`](https://github.com/williajm/glidepath/blob/main/CHANGELOG.md)
|
|
241
|
-
with the same artifacts
|
|
268
|
+
with the same Python artifacts and a tested Windows x64 executable attached,
|
|
269
|
+
each carrying signed build
|
|
242
270
|
provenance — verify a downloaded file with
|
|
243
271
|
`gh attestation verify <file> -R williajm/glidepath`. The signed
|
|
244
272
|
provenance bundle is attached to each release too
|
|
245
273
|
(`glidepath-X.Y.Z-provenance.intoto.jsonl`), so verification also
|
|
246
|
-
works offline with `--bundle`.
|
|
247
|
-
|
|
248
|
-
|
|
274
|
+
works offline with `--bundle`.
|
|
275
|
+
|
|
276
|
+
From v1.1.0, the tag-triggered workflow also builds
|
|
277
|
+
`glidepath-X.Y.Z-windows-x64.exe` with Nuitka. Download it from the
|
|
278
|
+
GitHub Release and double-click to run; Python and uv are not required.
|
|
279
|
+
The Windows build runs only for release tags, after tag validation, and
|
|
280
|
+
must pass its compiled-app checks before publication. Ordinary branch
|
|
281
|
+
pushes and PRs run the normal CI checks without compiling an executable.
|
|
282
|
+
The executable is not yet Authenticode-signed, so Windows may show a
|
|
283
|
+
SmartScreen warning; the build provenance above is a separate mechanism.
|
|
284
|
+
See [`docs/packaging.md`](https://github.com/williajm/glidepath/blob/main/docs/packaging.md)
|
|
285
|
+
for local builds and the release workflow.
|
|
249
286
|
|
|
250
287
|
## Data licences
|
|
251
288
|
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
19
19
|
<a href="https://williajm.github.io/glidepath/"><b>williajm.github.io/glidepath</b></a>
|
|
20
|
+
· <a href="https://pyrank.org/package/glidepath/">PyPI and GitHub stats on PyRank</a>
|
|
20
21
|
</p>
|
|
21
22
|
|
|
22
23
|
A desktop retirement and investment planner. UK-first, built so other
|
|
@@ -104,7 +105,32 @@ based solely on this tool.
|
|
|
104
105
|
|
|
105
106
|
## Install
|
|
106
107
|
|
|
107
|
-
|
|
108
|
+
### Windows download
|
|
109
|
+
|
|
110
|
+
From **v1.1.0**, Windows x64 users can run Glidepath without Python, uv
|
|
111
|
+
or terminal commands:
|
|
112
|
+
|
|
113
|
+
1. Open the [latest GitHub Release](https://github.com/williajm/glidepath/releases/latest).
|
|
114
|
+
2. Under **Assets**, download `glidepath-X.Y.Z-windows-x64.exe`.
|
|
115
|
+
3. Keep it in a folder of your choice and double-click it to launch.
|
|
116
|
+
|
|
117
|
+
This is a runnable app, not an installer; it does not add shortcuts or
|
|
118
|
+
update itself. For a newer version, close Glidepath and download the new
|
|
119
|
+
executable. Your saved `.glidepath.json` plans stay where you put them.
|
|
120
|
+
|
|
121
|
+
The executable is **not yet signed with a Windows publisher certificate**.
|
|
122
|
+
Windows may show “Windows protected your PC” or “unrecognised app”. Check
|
|
123
|
+
that the file came from this repository's release before deciding whether
|
|
124
|
+
to proceed. GitHub build provenance does not remove that warning.
|
|
125
|
+
If the latest release has no `.exe` yet, use the uv installation below.
|
|
126
|
+
|
|
127
|
+
Runtime licences and attribution notices are included in the executable
|
|
128
|
+
and in the release's `glidepath-X.Y.Z-third-party-licences.zip` download.
|
|
129
|
+
That archive also lists the corresponding upstream sources.
|
|
130
|
+
|
|
131
|
+
### Install with uv
|
|
132
|
+
|
|
133
|
+
Alternatively, install Glidepath from [PyPI](https://pypi.org/project/glidepath/)
|
|
108
134
|
with [uv](https://docs.astral.sh/uv/), which also fetches the Python it
|
|
109
135
|
needs — you do not need Python installed first.
|
|
110
136
|
|
|
@@ -210,14 +236,24 @@ Releases are `vX.Y.Z` tags on `main`. Each one is published to
|
|
|
210
236
|
trusted publishing with PEP 740 attestations, and as a GitHub Release
|
|
211
237
|
carrying its notes from
|
|
212
238
|
[`CHANGELOG.md`](https://github.com/williajm/glidepath/blob/main/CHANGELOG.md)
|
|
213
|
-
with the same artifacts
|
|
239
|
+
with the same Python artifacts and a tested Windows x64 executable attached,
|
|
240
|
+
each carrying signed build
|
|
214
241
|
provenance — verify a downloaded file with
|
|
215
242
|
`gh attestation verify <file> -R williajm/glidepath`. The signed
|
|
216
243
|
provenance bundle is attached to each release too
|
|
217
244
|
(`glidepath-X.Y.Z-provenance.intoto.jsonl`), so verification also
|
|
218
|
-
works offline with `--bundle`.
|
|
219
|
-
|
|
220
|
-
|
|
245
|
+
works offline with `--bundle`.
|
|
246
|
+
|
|
247
|
+
From v1.1.0, the tag-triggered workflow also builds
|
|
248
|
+
`glidepath-X.Y.Z-windows-x64.exe` with Nuitka. Download it from the
|
|
249
|
+
GitHub Release and double-click to run; Python and uv are not required.
|
|
250
|
+
The Windows build runs only for release tags, after tag validation, and
|
|
251
|
+
must pass its compiled-app checks before publication. Ordinary branch
|
|
252
|
+
pushes and PRs run the normal CI checks without compiling an executable.
|
|
253
|
+
The executable is not yet Authenticode-signed, so Windows may show a
|
|
254
|
+
SmartScreen warning; the build provenance above is a separate mechanism.
|
|
255
|
+
See [`docs/packaging.md`](https://github.com/williajm/glidepath/blob/main/docs/packaging.md)
|
|
256
|
+
for local builds and the release workflow.
|
|
221
257
|
|
|
222
258
|
## Data licences
|
|
223
259
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "glidepath"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.1.1"
|
|
4
4
|
description = "Desktop retirement and investment planner (UK-first, region-extensible)."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
# MIT code plus one CC BY-NC-SA 4.0 data file (returns_history.toml —
|
|
@@ -49,11 +49,18 @@ Homepage = "https://williajm.github.io/glidepath/"
|
|
|
49
49
|
Repository = "https://github.com/williajm/glidepath"
|
|
50
50
|
Changelog = "https://github.com/williajm/glidepath/blob/main/CHANGELOG.md"
|
|
51
51
|
Issues = "https://github.com/williajm/glidepath/issues"
|
|
52
|
+
Statistics = "https://pyrank.org/package/glidepath/"
|
|
52
53
|
|
|
53
54
|
[project.gui-scripts]
|
|
54
55
|
glidepath = "glidepath.gui.main:main"
|
|
55
56
|
|
|
56
57
|
[dependency-groups]
|
|
58
|
+
binary = [
|
|
59
|
+
"nuitka>=4.2",
|
|
60
|
+
"setuptools>=84",
|
|
61
|
+
"wheel>=0.48",
|
|
62
|
+
"zstandard",
|
|
63
|
+
]
|
|
57
64
|
dev = [
|
|
58
65
|
"hypothesis",
|
|
59
66
|
"mypy",
|
|
@@ -70,11 +77,36 @@ dev = [
|
|
|
70
77
|
requires = ["uv_build>=0.11,<0.13"]
|
|
71
78
|
build-backend = "uv_build"
|
|
72
79
|
|
|
80
|
+
# Shared by the local build command and the tagged release workflow.
|
|
81
|
+
[tool.glidepath.binary]
|
|
82
|
+
windows-icon = "src/glidepath/gui/assets/glidepath.ico"
|
|
83
|
+
# Dependency Walker 2.2.6000 (2006), required by Nuitka on Windows x64.
|
|
84
|
+
# Build tool only: never included in the distributed application.
|
|
85
|
+
dependency-walker-url = "https://dependencywalker.com/depends22_x64.zip"
|
|
86
|
+
dependency-walker-sha256 = "35db68a613874a2e8c1422eb0ea7861f825fc71717d46dabf1f249ce9634b4f1"
|
|
87
|
+
nuitka-options = [
|
|
88
|
+
"--update-check=never",
|
|
89
|
+
"--enable-plugin=pyside6",
|
|
90
|
+
"--include-qt-plugins=platforms,imageformats",
|
|
91
|
+
"--include-package-data=glidepath",
|
|
92
|
+
"--include-distribution-metadata=glidepath",
|
|
93
|
+
"--include-data-files=LICENSE=LICENSE",
|
|
94
|
+
"--include-data-files=LICENSE-DATA=LICENSE-DATA",
|
|
95
|
+
"--include-data-dir=third-party-licences=third-party-licences",
|
|
96
|
+
"--product-name=Glidepath",
|
|
97
|
+
"--file-description=Glidepath retirement planner",
|
|
98
|
+
"--company-name=James Williams",
|
|
99
|
+
]
|
|
100
|
+
|
|
73
101
|
# --- uv ---------------------------------------------------------------------
|
|
74
102
|
[tool.uv]
|
|
75
103
|
# Supply-chain cooldown: resolution ignores anything published to PyPI after
|
|
76
104
|
# this timestamp (now minus 7 days). Updated ONLY by `make deps`.
|
|
77
|
-
exclude-newer = "2026-08-
|
|
105
|
+
exclude-newer = "2026-08-29T16:19:47Z"
|
|
106
|
+
# Nuitka is source-only. uv installs its build requirements from the binary
|
|
107
|
+
# group first, then builds it in that environment: the lockfile hashes, age
|
|
108
|
+
# check and all-groups audit cover the exact setuptools/wheel versions used.
|
|
109
|
+
no-build-isolation-package = ["nuitka"]
|
|
78
110
|
|
|
79
111
|
# PyPI is the ONLY permitted index (supply-chain policy; see CLAUDE.md).
|
|
80
112
|
[[tool.uv.index]]
|
|
@@ -60,7 +60,7 @@ SAVE_DIALOG_TITLE: Final = "Save plan"
|
|
|
60
60
|
UNSAVED_CHANGES_TITLE: Final = "Unsaved changes"
|
|
61
61
|
|
|
62
62
|
UNSAVED_CHANGES_PROMPT: Final = (
|
|
63
|
-
"This plan has unsaved changes. Save them before
|
|
63
|
+
"This plan has unsaved changes. Save them before continuing?"
|
|
64
64
|
)
|
|
65
65
|
|
|
66
66
|
NOTHING_TO_SAVE_MESSAGE: Final = (
|
|
@@ -11,11 +11,14 @@ candidate: the returned age is exactly the earliest succeeding one
|
|
|
11
11
|
even when success is not monotone in age (a DB scheme's early-payment
|
|
12
12
|
factors or a dated outflow can make it dip), and every answer was
|
|
13
13
|
actually probed, never interpolated. Household spending begins once
|
|
14
|
-
|
|
14
|
+
every living person has retired (planning §4.11), so a candidate has a
|
|
15
15
|
retired period to test the income in only when the whole household —
|
|
16
16
|
the candidate age for the selected person, the partner's stated
|
|
17
17
|
decision held fixed — is retired inside the run's horizon; a
|
|
18
|
-
candidate without one fails rather than succeeding vacuously.
|
|
18
|
+
candidate without one fails rather than succeeding vacuously. Death
|
|
19
|
+
uses the engine's period-start gate: at least one person must remain
|
|
20
|
+
alive, and a deceased partner's retirement age no longer constrains
|
|
21
|
+
the survivor's spending.
|
|
19
22
|
|
|
20
23
|
Each probe replaces the *selected* person's retirement-age decision
|
|
21
24
|
with the candidate — a partner's decision is held fixed (planning
|
|
@@ -142,11 +145,9 @@ def earliest_retirement_age( # noqa: PLR0913
|
|
|
142
145
|
returns the first that succeeds — exactly the earliest, whatever
|
|
143
146
|
the success shape over ages (module docstring) — or ``None`` when
|
|
144
147
|
no age in the bracket does. A candidate with no *retirement
|
|
145
|
-
exposure* — no projected period opening
|
|
146
|
-
retired under the §4.1 gate
|
|
147
|
-
|
|
148
|
-
a partner's fixed retirement date falls at or past the run's
|
|
149
|
-
horizon — never tests the target income at all, so it fails
|
|
148
|
+
exposure* — no projected period opening with at least one living
|
|
149
|
+
person and all living persons retired under the §4.1 gate
|
|
150
|
+
convention — never tests the target income at all, so it fails
|
|
150
151
|
rather than succeeding vacuously; such candidates are never
|
|
151
152
|
probed. The plan's stated retirement age and spending level are
|
|
152
153
|
irrelevant to the search: each probe carries the candidate age and
|
|
@@ -168,7 +169,7 @@ def earliest_retirement_age( # noqa: PLR0913
|
|
|
168
169
|
periods = _projected_periods(plan, assumptions, region, config)
|
|
169
170
|
|
|
170
171
|
def has_retired_period(age: int) -> bool:
|
|
171
|
-
"""Whether any period opens
|
|
172
|
+
"""Whether any period opens a living household retired (§4.1)."""
|
|
172
173
|
return any(
|
|
173
174
|
_household_retired_by(period, plan.persons, selected.id, age)
|
|
174
175
|
for period in periods
|
|
@@ -227,12 +228,11 @@ def sustainable_income_at_age( # noqa: PLR0913
|
|
|
227
228
|
age and the candidate spending instead, everything else unchanged.
|
|
228
229
|
|
|
229
230
|
An ``age`` with no *retirement exposure* — no projected period
|
|
230
|
-
opening
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
would succeed vacuously. It answers ``None`` without probing,
|
|
231
|
+
opening with at least one living person and all living persons
|
|
232
|
+
retired under the §4.1 gate convention — has no retired period to
|
|
233
|
+
test any income in: spending is modelled only in a living
|
|
234
|
+
household's retirement, so every level would succeed vacuously.
|
|
235
|
+
It answers ``None`` without probing,
|
|
236
236
|
exactly as such candidates fail in the age search. ``None``
|
|
237
237
|
otherwise means what the income search means by it: not even zero
|
|
238
238
|
spending survives the plan's outflows.
|
|
@@ -303,21 +303,31 @@ def _household_retired_by(
|
|
|
303
303
|
selected_id: EntityId,
|
|
304
304
|
age: int,
|
|
305
305
|
) -> bool:
|
|
306
|
-
"""Whether
|
|
306
|
+
"""Whether a living household opens ``period`` retired (§4.1, §4.11).
|
|
307
307
|
|
|
308
308
|
The exposure predicate of both solvers: the selected person
|
|
309
309
|
retires at the probed ``age`` while a partner's stated
|
|
310
|
-
retirement-age decision is held fixed
|
|
311
|
-
|
|
312
|
-
|
|
310
|
+
retirement-age decision is held fixed. Match the engine's death
|
|
311
|
+
gate: a person is no longer living once their death age has been
|
|
312
|
+
attained by the period start. Spending begins once all living
|
|
313
|
+
persons retire and stops when none remain, so only such a period
|
|
314
|
+
can test retirement income.
|
|
313
315
|
"""
|
|
314
|
-
|
|
316
|
+
living = tuple(
|
|
317
|
+
person
|
|
318
|
+
for person in persons
|
|
319
|
+
if person.death_age is None
|
|
320
|
+
or not is_age_attained_by_period_start(
|
|
321
|
+
person.date_of_birth.value, person.death_age.value, period
|
|
322
|
+
)
|
|
323
|
+
)
|
|
324
|
+
return bool(living) and all(
|
|
315
325
|
is_age_attained_by_period_start(
|
|
316
326
|
person.date_of_birth.value,
|
|
317
327
|
age if person.id == selected_id else person.target_retirement_age.value,
|
|
318
328
|
period,
|
|
319
329
|
)
|
|
320
|
-
for person in
|
|
330
|
+
for person in living
|
|
321
331
|
)
|
|
322
332
|
|
|
323
333
|
|
|
Binary file
|
|
@@ -393,7 +393,7 @@ class FactsEntryPane(QWidget):
|
|
|
393
393
|
self,
|
|
394
394
|
view_model: FactsFormViewModel,
|
|
395
395
|
on_submit: Callable[[FactsFormData], FactsSubmissionOutcome],
|
|
396
|
-
on_clear: Callable[[], str],
|
|
396
|
+
on_clear: Callable[[], str | None],
|
|
397
397
|
parent: QWidget | None = None,
|
|
398
398
|
) -> None:
|
|
399
399
|
"""Render the form and wire the submit and clear callbacks."""
|
|
@@ -754,10 +754,13 @@ class FactsEntryPane(QWidget):
|
|
|
754
754
|
self._show_errors(outcome.errors)
|
|
755
755
|
|
|
756
756
|
def clear(self) -> None:
|
|
757
|
-
"""Empty every section
|
|
757
|
+
"""Empty every section only when the callback allows the clear."""
|
|
758
|
+
status = self._on_clear()
|
|
759
|
+
if status is None:
|
|
760
|
+
return
|
|
758
761
|
self._clear_all_errors()
|
|
759
762
|
self.set_form_data(FactsFormData())
|
|
760
|
-
self.status_label.setText(
|
|
763
|
+
self.status_label.setText(status)
|
|
761
764
|
|
|
762
765
|
|
|
763
766
|
__all__ = [
|
|
@@ -381,6 +381,8 @@ class MainWindow(QMainWindow):
|
|
|
381
381
|
starts from what the file says. On failure the session is left
|
|
382
382
|
untouched and the status bar explains.
|
|
383
383
|
"""
|
|
384
|
+
if not self._confirm_plan_replacement():
|
|
385
|
+
return False
|
|
384
386
|
outcome = load_plan_state(path, today=_today())
|
|
385
387
|
self.statusBar().showMessage(outcome.message)
|
|
386
388
|
if outcome.state is None:
|
|
@@ -545,17 +547,21 @@ class MainWindow(QMainWindow):
|
|
|
545
547
|
self._remember_plan_path(path)
|
|
546
548
|
|
|
547
549
|
def closeEvent(self, event: QCloseEvent) -> None: # noqa: N802
|
|
548
|
-
"""
|
|
550
|
+
"""Close only after unsaved plan edits have been dealt with."""
|
|
551
|
+
if self._confirm_plan_replacement():
|
|
552
|
+
event.accept()
|
|
553
|
+
else:
|
|
554
|
+
event.ignore()
|
|
555
|
+
|
|
556
|
+
def _confirm_plan_replacement(self) -> bool:
|
|
557
|
+
"""Ask before closing, opening, or clearing discards plan edits.
|
|
549
558
|
|
|
550
559
|
Save runs the normal save flow — asking for a path when the
|
|
551
|
-
session has none
|
|
552
|
-
|
|
553
|
-
rather than silently discarding the edits it just promised to
|
|
554
|
-
keep.
|
|
560
|
+
session has none. A failed or cancelled save abandons the
|
|
561
|
+
pending action and preserves the session and form.
|
|
555
562
|
"""
|
|
556
563
|
if not has_unsaved_changes(self._state):
|
|
557
|
-
|
|
558
|
-
return
|
|
564
|
+
return True
|
|
559
565
|
choice = QMessageBox.question(
|
|
560
566
|
self,
|
|
561
567
|
UNSAVED_CHANGES_TITLE,
|
|
@@ -567,15 +573,8 @@ class MainWindow(QMainWindow):
|
|
|
567
573
|
)
|
|
568
574
|
if choice == QMessageBox.StandardButton.Save:
|
|
569
575
|
self.save_plan()
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
return
|
|
573
|
-
event.accept()
|
|
574
|
-
return
|
|
575
|
-
if choice == QMessageBox.StandardButton.Discard:
|
|
576
|
-
event.accept()
|
|
577
|
-
return
|
|
578
|
-
event.ignore()
|
|
576
|
+
return not has_unsaved_changes(self._state)
|
|
577
|
+
return choice == QMessageBox.StandardButton.Discard
|
|
579
578
|
|
|
580
579
|
def _remember_plan_path(self, path: Path) -> None:
|
|
581
580
|
"""Record the plan path for the next launch, best-effort.
|
|
@@ -611,13 +610,15 @@ class MainWindow(QMainWindow):
|
|
|
611
610
|
self._state = state_marked_saved(self._state)
|
|
612
611
|
self.facts_pane.status_label.setText(self._view_model.facts_form.example_note)
|
|
613
612
|
|
|
614
|
-
def _handle_cleared(self) -> str:
|
|
613
|
+
def _handle_cleared(self) -> str | None:
|
|
615
614
|
"""Reset the session to no plan and re-render the result panes.
|
|
616
615
|
|
|
617
616
|
The session's plan file detaches too: after a clear, the next
|
|
618
617
|
Save asks where to write rather than overwriting the file the
|
|
619
618
|
cleared plan came from.
|
|
620
619
|
"""
|
|
620
|
+
if not self._confirm_plan_replacement():
|
|
621
|
+
return None
|
|
621
622
|
self._state = initial_plan_state()
|
|
622
623
|
self._plan_path = None
|
|
623
624
|
self._refresh_result_panes()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|