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.
Files changed (93) hide show
  1. glidepath/__init__.py +3 -0
  2. glidepath/app/__init__.py +364 -0
  3. glidepath/app/backtest.py +281 -0
  4. glidepath/app/charts.py +759 -0
  5. glidepath/app/copy.py +174 -0
  6. glidepath/app/display.py +148 -0
  7. glidepath/app/drawdown.py +436 -0
  8. glidepath/app/example.py +66 -0
  9. glidepath/app/exports.py +487 -0
  10. glidepath/app/files.py +249 -0
  11. glidepath/app/firstrun.py +114 -0
  12. glidepath/app/forms.py +1750 -0
  13. glidepath/app/inspector.py +506 -0
  14. glidepath/app/labels.py +66 -0
  15. glidepath/app/montecarlo.py +399 -0
  16. glidepath/app/plan.py +354 -0
  17. glidepath/app/retirement.py +446 -0
  18. glidepath/app/scenarios.py +831 -0
  19. glidepath/app/shell.py +185 -0
  20. glidepath/app/tables.py +138 -0
  21. glidepath/core/__init__.py +390 -0
  22. glidepath/core/annuities.py +240 -0
  23. glidepath/core/backtest.py +514 -0
  24. glidepath/core/comparison.py +278 -0
  25. glidepath/core/config.py +82 -0
  26. glidepath/core/contributions.py +337 -0
  27. glidepath/core/engine.py +2811 -0
  28. glidepath/core/entities.py +264 -0
  29. glidepath/core/glide.py +289 -0
  30. glidepath/core/investments.py +175 -0
  31. glidepath/core/money.py +107 -0
  32. glidepath/core/montecarlo.py +609 -0
  33. glidepath/core/pensions.py +298 -0
  34. glidepath/core/periods.py +367 -0
  35. glidepath/core/provenance.py +271 -0
  36. glidepath/core/randomness.py +128 -0
  37. glidepath/core/region.py +46 -0
  38. glidepath/core/reporting.py +231 -0
  39. glidepath/core/results.py +504 -0
  40. glidepath/core/retirement.py +291 -0
  41. glidepath/core/returns.py +312 -0
  42. glidepath/core/scenarios.py +579 -0
  43. glidepath/core/state_pension.py +264 -0
  44. glidepath/core/tax.py +139 -0
  45. glidepath/core/withdrawals.py +461 -0
  46. glidepath/core/wrappers.py +278 -0
  47. glidepath/gui/__init__.py +6 -0
  48. glidepath/gui/assets/icon_128.png +0 -0
  49. glidepath/gui/assets/icon_16.png +0 -0
  50. glidepath/gui/assets/icon_24.png +0 -0
  51. glidepath/gui/assets/icon_256.png +0 -0
  52. glidepath/gui/assets/icon_32.png +0 -0
  53. glidepath/gui/assets/icon_48.png +0 -0
  54. glidepath/gui/assets/icon_64.png +0 -0
  55. glidepath/gui/assets/wordmark.png +0 -0
  56. glidepath/gui/charts.py +829 -0
  57. glidepath/gui/forms.py +359 -0
  58. glidepath/gui/inspector.py +186 -0
  59. glidepath/gui/main.py +51 -0
  60. glidepath/gui/scenarios.py +402 -0
  61. glidepath/gui/style.py +376 -0
  62. glidepath/gui/tableview.py +67 -0
  63. glidepath/gui/widgets.py +989 -0
  64. glidepath/persistence/__init__.py +48 -0
  65. glidepath/persistence/assumptions.py +112 -0
  66. glidepath/persistence/decode.py +747 -0
  67. glidepath/persistence/document.py +101 -0
  68. glidepath/persistence/encode.py +433 -0
  69. glidepath/persistence/migrations.py +158 -0
  70. glidepath/persistence/values.py +298 -0
  71. glidepath/py.typed +0 -0
  72. glidepath/regions/__init__.py +7 -0
  73. glidepath/regions/uk/__init__.py +189 -0
  74. glidepath/regions/uk/ages.py +156 -0
  75. glidepath/regions/uk/contributions.py +717 -0
  76. glidepath/regions/uk/data/age_rules.toml +78 -0
  77. glidepath/regions/uk/data/assumptions_default.toml +170 -0
  78. glidepath/regions/uk/data/returns_history.toml +150 -0
  79. glidepath/regions/uk/data/tax_year_2026_27.toml +98 -0
  80. glidepath/regions/uk/extension.py +479 -0
  81. glidepath/regions/uk/loader.py +704 -0
  82. glidepath/regions/uk/region.py +160 -0
  83. glidepath/regions/uk/schema.py +563 -0
  84. glidepath/regions/uk/state_pension.py +129 -0
  85. glidepath/regions/uk/tax.py +466 -0
  86. glidepath/regions/uk/wrappers.py +283 -0
  87. glidepath/regions/uk/years.py +92 -0
  88. glidepath-0.2.0.dist-info/METADATA +189 -0
  89. glidepath-0.2.0.dist-info/RECORD +93 -0
  90. glidepath-0.2.0.dist-info/WHEEL +4 -0
  91. glidepath-0.2.0.dist-info/entry_points.txt +3 -0
  92. glidepath-0.2.0.dist-info/licenses/LICENSE +21 -0
  93. 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)