glidepath 0.2.0__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.
- glidepath/__init__.py +3 -0
- glidepath/app/__init__.py +364 -0
- glidepath/app/backtest.py +281 -0
- glidepath/app/charts.py +759 -0
- glidepath/app/copy.py +174 -0
- glidepath/app/display.py +148 -0
- glidepath/app/drawdown.py +436 -0
- glidepath/app/example.py +66 -0
- glidepath/app/exports.py +487 -0
- glidepath/app/files.py +249 -0
- glidepath/app/firstrun.py +114 -0
- glidepath/app/forms.py +1750 -0
- glidepath/app/inspector.py +506 -0
- glidepath/app/labels.py +66 -0
- glidepath/app/montecarlo.py +399 -0
- glidepath/app/plan.py +354 -0
- glidepath/app/retirement.py +446 -0
- glidepath/app/scenarios.py +831 -0
- glidepath/app/shell.py +185 -0
- glidepath/app/tables.py +138 -0
- glidepath/core/__init__.py +390 -0
- glidepath/core/annuities.py +240 -0
- glidepath/core/backtest.py +514 -0
- glidepath/core/comparison.py +278 -0
- glidepath/core/config.py +82 -0
- glidepath/core/contributions.py +337 -0
- glidepath/core/engine.py +2811 -0
- glidepath/core/entities.py +264 -0
- glidepath/core/glide.py +289 -0
- glidepath/core/investments.py +175 -0
- glidepath/core/money.py +107 -0
- glidepath/core/montecarlo.py +609 -0
- glidepath/core/pensions.py +298 -0
- glidepath/core/periods.py +367 -0
- glidepath/core/provenance.py +271 -0
- glidepath/core/randomness.py +128 -0
- glidepath/core/region.py +46 -0
- glidepath/core/reporting.py +231 -0
- glidepath/core/results.py +504 -0
- glidepath/core/retirement.py +291 -0
- glidepath/core/returns.py +312 -0
- glidepath/core/scenarios.py +579 -0
- glidepath/core/state_pension.py +264 -0
- glidepath/core/tax.py +139 -0
- glidepath/core/withdrawals.py +461 -0
- glidepath/core/wrappers.py +278 -0
- glidepath/gui/__init__.py +6 -0
- glidepath/gui/assets/icon_128.png +0 -0
- glidepath/gui/assets/icon_16.png +0 -0
- glidepath/gui/assets/icon_24.png +0 -0
- glidepath/gui/assets/icon_256.png +0 -0
- glidepath/gui/assets/icon_32.png +0 -0
- glidepath/gui/assets/icon_48.png +0 -0
- glidepath/gui/assets/icon_64.png +0 -0
- glidepath/gui/assets/wordmark.png +0 -0
- glidepath/gui/charts.py +829 -0
- glidepath/gui/forms.py +359 -0
- glidepath/gui/inspector.py +186 -0
- glidepath/gui/main.py +51 -0
- glidepath/gui/scenarios.py +402 -0
- glidepath/gui/style.py +376 -0
- glidepath/gui/tableview.py +67 -0
- glidepath/gui/widgets.py +989 -0
- glidepath/persistence/__init__.py +48 -0
- glidepath/persistence/assumptions.py +112 -0
- glidepath/persistence/decode.py +747 -0
- glidepath/persistence/document.py +101 -0
- glidepath/persistence/encode.py +433 -0
- glidepath/persistence/migrations.py +158 -0
- glidepath/persistence/values.py +298 -0
- glidepath/py.typed +0 -0
- glidepath/regions/__init__.py +7 -0
- glidepath/regions/uk/__init__.py +189 -0
- glidepath/regions/uk/ages.py +156 -0
- glidepath/regions/uk/contributions.py +717 -0
- glidepath/regions/uk/data/age_rules.toml +78 -0
- glidepath/regions/uk/data/assumptions_default.toml +170 -0
- glidepath/regions/uk/data/returns_history.toml +150 -0
- glidepath/regions/uk/data/tax_year_2026_27.toml +98 -0
- glidepath/regions/uk/extension.py +479 -0
- glidepath/regions/uk/loader.py +704 -0
- glidepath/regions/uk/region.py +160 -0
- glidepath/regions/uk/schema.py +563 -0
- glidepath/regions/uk/state_pension.py +129 -0
- glidepath/regions/uk/tax.py +466 -0
- glidepath/regions/uk/wrappers.py +283 -0
- glidepath/regions/uk/years.py +92 -0
- glidepath-0.2.0.dist-info/METADATA +189 -0
- glidepath-0.2.0.dist-info/RECORD +93 -0
- glidepath-0.2.0.dist-info/WHEEL +4 -0
- glidepath-0.2.0.dist-info/entry_points.txt +3 -0
- glidepath-0.2.0.dist-info/licenses/LICENSE +21 -0
- glidepath-0.2.0.dist-info/licenses/LICENSE-DATA +28 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""``.glidepath.json`` persistence (roadmap 6.2, 6.4; planning §4.5).
|
|
2
|
+
|
|
3
|
+
One deterministic JSON document per plan: the household, its
|
|
4
|
+
scenarios, and the user's assumption overrides — never resolved
|
|
5
|
+
assumption values, which re-resolve on load against the region's
|
|
6
|
+
current shipped data. The reader runs every file through the versioned
|
|
7
|
+
migration harness first, so old files keep opening as the schema
|
|
8
|
+
evolves. Region-agnostic like the core: the region's defaults and data
|
|
9
|
+
version are inputs, never imports.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from glidepath.persistence.assumptions import (
|
|
13
|
+
assumption_overrides_from,
|
|
14
|
+
resolve_assumptions,
|
|
15
|
+
)
|
|
16
|
+
from glidepath.persistence.decode import load_plan, loads_plan
|
|
17
|
+
from glidepath.persistence.document import (
|
|
18
|
+
SCHEMA_VERSION,
|
|
19
|
+
AssumptionOverride,
|
|
20
|
+
PersistenceError,
|
|
21
|
+
PlanDocument,
|
|
22
|
+
)
|
|
23
|
+
from glidepath.persistence.encode import dumps_plan, save_plan
|
|
24
|
+
from glidepath.persistence.migrations import (
|
|
25
|
+
UPGRADERS,
|
|
26
|
+
RawDocument,
|
|
27
|
+
Upgrader,
|
|
28
|
+
apply_migrations,
|
|
29
|
+
document_schema_version,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"SCHEMA_VERSION",
|
|
34
|
+
"UPGRADERS",
|
|
35
|
+
"AssumptionOverride",
|
|
36
|
+
"PersistenceError",
|
|
37
|
+
"PlanDocument",
|
|
38
|
+
"RawDocument",
|
|
39
|
+
"Upgrader",
|
|
40
|
+
"apply_migrations",
|
|
41
|
+
"assumption_overrides_from",
|
|
42
|
+
"document_schema_version",
|
|
43
|
+
"dumps_plan",
|
|
44
|
+
"load_plan",
|
|
45
|
+
"loads_plan",
|
|
46
|
+
"resolve_assumptions",
|
|
47
|
+
"save_plan",
|
|
48
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Assumption re-resolution on load (roadmap 6.2; planning §4.5).
|
|
2
|
+
|
|
3
|
+
User files store only assumption *overrides*: on every load the
|
|
4
|
+
effective set is rebuilt from the region's current shipped defaults
|
|
5
|
+
with the stored overrides applied on top, each carrying
|
|
6
|
+
``USER_OVERRIDE`` provenance and the current shipped default alongside
|
|
7
|
+
its value. A default that moved since the file was last saved therefore
|
|
8
|
+
changes the plan visibly — the document's recorded data version tells
|
|
9
|
+
the caller to surface it (planning §4.5).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from collections.abc import Mapping
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from glidepath.core import Assumption, AssumptionSet, Provenance
|
|
16
|
+
from glidepath.persistence.document import AssumptionOverride, PersistenceError
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from glidepath.persistence.document import PlanDocument
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def resolve_assumptions(
|
|
23
|
+
document: PlanDocument, defaults: AssumptionSet
|
|
24
|
+
) -> AssumptionSet:
|
|
25
|
+
"""The effective assumptions: shipped defaults ⊕ stored overrides.
|
|
26
|
+
|
|
27
|
+
``defaults`` is the region's current shipped set (e.g.
|
|
28
|
+
``glidepath.regions.uk.default_assumption_set()``); the core stays
|
|
29
|
+
region-agnostic by taking it as an input. Each override keeps the
|
|
30
|
+
shipped entry's default value and description, so the UI can always
|
|
31
|
+
answer "what would this have been?".
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
PersistenceError: If an override targets a key the defaults do
|
|
35
|
+
not register, or its value's shape no longer matches the
|
|
36
|
+
shipped default's.
|
|
37
|
+
"""
|
|
38
|
+
pending = {override.key: override for override in document.assumption_overrides}
|
|
39
|
+
resolved = []
|
|
40
|
+
for key in defaults.keys:
|
|
41
|
+
base = defaults.get(key)
|
|
42
|
+
override = pending.pop(key, None)
|
|
43
|
+
resolved.append(base if override is None else _overridden(base, override))
|
|
44
|
+
if pending:
|
|
45
|
+
unknown = ", ".join(sorted(key.value for key in pending))
|
|
46
|
+
msg = f"assumption overrides target unregistered keys: {unknown}"
|
|
47
|
+
raise PersistenceError(msg)
|
|
48
|
+
return AssumptionSet(resolved)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def assumption_overrides_from(
|
|
52
|
+
assumptions: AssumptionSet,
|
|
53
|
+
) -> tuple[AssumptionOverride, ...]:
|
|
54
|
+
"""The set's user overrides as storable records, sorted by key.
|
|
55
|
+
|
|
56
|
+
The inverse of :func:`resolve_assumptions` for saving: only
|
|
57
|
+
``USER_OVERRIDE`` entries persist (planning §4.5) — defaults are
|
|
58
|
+
never written, and scenario overrides live on their scenarios.
|
|
59
|
+
"""
|
|
60
|
+
return tuple(
|
|
61
|
+
AssumptionOverride(
|
|
62
|
+
key=entry.key,
|
|
63
|
+
value=entry.value,
|
|
64
|
+
source=entry.source,
|
|
65
|
+
recorded_on=entry.recorded_on,
|
|
66
|
+
)
|
|
67
|
+
for entry in (assumptions.get(key) for key in sorted(assumptions.keys))
|
|
68
|
+
if entry.provenance is Provenance.USER_OVERRIDE
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _overridden(base: Assumption[Any], override: AssumptionOverride) -> Assumption[Any]:
|
|
73
|
+
"""The shipped default re-stamped with the stored override."""
|
|
74
|
+
_check_value_shape(base, override)
|
|
75
|
+
return Assumption(
|
|
76
|
+
key=base.key,
|
|
77
|
+
value=override.value,
|
|
78
|
+
default_value=base.default_value,
|
|
79
|
+
provenance=Provenance.USER_OVERRIDE,
|
|
80
|
+
source=override.source,
|
|
81
|
+
recorded_on=override.recorded_on,
|
|
82
|
+
description=base.description,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _check_value_shape(base: Assumption[Any], override: AssumptionOverride) -> None:
|
|
87
|
+
"""Reject a stored value shaped unlike the current shipped default.
|
|
88
|
+
|
|
89
|
+
The same rule as scenario resolution (planning §4.3): a mapping
|
|
90
|
+
default accepts any mapping; a scalar default requires the exact
|
|
91
|
+
runtime type. A shipped default that changed shape since the file
|
|
92
|
+
was saved fails loudly rather than corrupting a run.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
PersistenceError: If the shapes no longer match.
|
|
96
|
+
"""
|
|
97
|
+
base_value = base.value
|
|
98
|
+
override_value = override.value
|
|
99
|
+
if isinstance(base_value, Mapping):
|
|
100
|
+
if isinstance(override_value, Mapping):
|
|
101
|
+
return
|
|
102
|
+
msg = (
|
|
103
|
+
f"stored override on {base.key.value!r} must hold a mapping,"
|
|
104
|
+
f" got {type(override_value).__name__}"
|
|
105
|
+
)
|
|
106
|
+
raise PersistenceError(msg)
|
|
107
|
+
if type(override_value) is not type(base_value):
|
|
108
|
+
msg = (
|
|
109
|
+
f"stored override on {base.key.value!r} must hold a"
|
|
110
|
+
f" {type(base_value).__name__}, got {type(override_value).__name__}"
|
|
111
|
+
)
|
|
112
|
+
raise PersistenceError(msg)
|