worthune 0.1.0__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.
@@ -0,0 +1,5 @@
1
+ node_modules/
2
+ dist/
3
+ __pycache__/
4
+ *.egg-info/
5
+ build/
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.5
2
+ Name: worthune
3
+ Version: 0.1.0
4
+ Summary: Official SDK for the Worthune Model API — 27 dual-implementation-verified financial calculation models with published specs and IRS/SSA-sourced constants. Zero dependencies.
5
+ Project-URL: Homepage, https://worthune.com
6
+ Project-URL: Documentation, https://worthune.com/docs
7
+ Project-URL: Repository, https://github.com/CapsteraSupport/worthune-sdk
8
+ Author: Worthune
9
+ License-Expression: MIT
10
+ Keywords: api-client,calculator,evals,finance,financial-calculations,financial-planning,fintech,mortgage,retirement,verified
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Financial and Insurance Industry
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Office/Business :: Financial
16
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+
20
+ # worthune
21
+
22
+ Zero-dependency SDK for the [Worthune Model API](https://worthune.com/docs) —
23
+ 27 financial calculation models you can cite, audit, and trust. No API keys.
24
+ Free with attribution.
25
+
26
+ Every model has a **published spec**, an **independent second implementation
27
+ that must agree** with the first on 250 fuzzed cases before anything ships,
28
+ and **IRS/SSA constants traced to primary sources**. Model changes are never
29
+ silent: responses pin `specVersion`, and the
30
+ [changelog is public](https://worthune.com/models/changelog).
31
+
32
+ ```bash
33
+ pip install worthune
34
+ ```
35
+
36
+ ```python
37
+ from worthune import Worthune, verify_record
38
+
39
+ client = Worthune()
40
+
41
+ result = client.run("relocation", {
42
+ "currentSalary": 95000, "newSalary": 108000,
43
+ "currentMonthlyExpenses": 4200, "newMonthlyExpenses": 4900,
44
+ "movingCosts": 6000, "currentSavings": 40000,
45
+ "annualReturn": 0.07, "yearsHorizon": 10,
46
+ })
47
+
48
+ result["outputs"]["breakEvenMonths"] # 16
49
+ result["specVersion"] # pinned contract version
50
+ result["facts"] # IRS/SSA constants used, with sources
51
+ result["sentinels"] # special values, explained
52
+ verify_record(result) # True — audit fingerprint checks out
53
+ ```
54
+
55
+ ## API
56
+
57
+ - `client.list_models()` — catalog with spec versions
58
+ - `client.get_contract(model)` — machine-readable inputs/domains/sentinels
59
+ - `client.get_spec(model)` — the full published spec (markdown)
60
+ - `client.run(model, inputs)` — run; validation failures return `{"ok": False, "errors": [...]}` instead of raising
61
+ - `client.get_eval_dataset(model)` — 250 verified input/expected-output pairs, the exact cases Worthune's dual-implementation CI verifies (ground truth for financial AI testing)
62
+ - `client.get_facts()` — the sourced IRS/SSA constants registry
63
+ - `verify_record(response)` — recompute the SHA-256 decision record to prove where numbers came from (byte-exact ECMAScript-compatible canonical JSON)
64
+
65
+ Retirement, FIRE, rent vs. buy, refinancing, equity comp (RSUs), debt payoff,
66
+ Social Security timing, 529 plans, estate planning, and more — the
67
+ [full catalog](https://worthune.com/models) lists all 27.
68
+
69
+ Fair use: 5,000 runs/month per app (a guideline, not a meter) —
70
+ [the free tier, in writing](https://worthune.com/pricing).
71
+
72
+ MIT licensed. The models, specs, and verification harness live behind the API
73
+ at [worthune.com](https://worthune.com).
@@ -0,0 +1,54 @@
1
+ # worthune
2
+
3
+ Zero-dependency SDK for the [Worthune Model API](https://worthune.com/docs) —
4
+ 27 financial calculation models you can cite, audit, and trust. No API keys.
5
+ Free with attribution.
6
+
7
+ Every model has a **published spec**, an **independent second implementation
8
+ that must agree** with the first on 250 fuzzed cases before anything ships,
9
+ and **IRS/SSA constants traced to primary sources**. Model changes are never
10
+ silent: responses pin `specVersion`, and the
11
+ [changelog is public](https://worthune.com/models/changelog).
12
+
13
+ ```bash
14
+ pip install worthune
15
+ ```
16
+
17
+ ```python
18
+ from worthune import Worthune, verify_record
19
+
20
+ client = Worthune()
21
+
22
+ result = client.run("relocation", {
23
+ "currentSalary": 95000, "newSalary": 108000,
24
+ "currentMonthlyExpenses": 4200, "newMonthlyExpenses": 4900,
25
+ "movingCosts": 6000, "currentSavings": 40000,
26
+ "annualReturn": 0.07, "yearsHorizon": 10,
27
+ })
28
+
29
+ result["outputs"]["breakEvenMonths"] # 16
30
+ result["specVersion"] # pinned contract version
31
+ result["facts"] # IRS/SSA constants used, with sources
32
+ result["sentinels"] # special values, explained
33
+ verify_record(result) # True — audit fingerprint checks out
34
+ ```
35
+
36
+ ## API
37
+
38
+ - `client.list_models()` — catalog with spec versions
39
+ - `client.get_contract(model)` — machine-readable inputs/domains/sentinels
40
+ - `client.get_spec(model)` — the full published spec (markdown)
41
+ - `client.run(model, inputs)` — run; validation failures return `{"ok": False, "errors": [...]}` instead of raising
42
+ - `client.get_eval_dataset(model)` — 250 verified input/expected-output pairs, the exact cases Worthune's dual-implementation CI verifies (ground truth for financial AI testing)
43
+ - `client.get_facts()` — the sourced IRS/SSA constants registry
44
+ - `verify_record(response)` — recompute the SHA-256 decision record to prove where numbers came from (byte-exact ECMAScript-compatible canonical JSON)
45
+
46
+ Retirement, FIRE, rent vs. buy, refinancing, equity comp (RSUs), debt payoff,
47
+ Social Security timing, 529 plans, estate planning, and more — the
48
+ [full catalog](https://worthune.com/models) lists all 27.
49
+
50
+ Fair use: 5,000 runs/month per app (a guideline, not a meter) —
51
+ [the free tier, in writing](https://worthune.com/pricing).
52
+
53
+ MIT licensed. The models, specs, and verification harness live behind the API
54
+ at [worthune.com](https://worthune.com).
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "worthune"
7
+ version = "0.1.0"
8
+ description = "Official SDK for the Worthune Model API — 27 dual-implementation-verified financial calculation models with published specs and IRS/SSA-sourced constants. Zero dependencies."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.9"
12
+ authors = [{ name = "Worthune" }]
13
+ keywords = [
14
+ "finance",
15
+ "financial-calculations",
16
+ "fintech",
17
+ "retirement",
18
+ "mortgage",
19
+ "verified",
20
+ "calculator",
21
+ "api-client",
22
+ "financial-planning",
23
+ "evals",
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Intended Audience :: Developers",
28
+ "Intended Audience :: Financial and Insurance Industry",
29
+ "Programming Language :: Python :: 3",
30
+ "Topic :: Office/Business :: Financial",
31
+ "Topic :: Software Development :: Libraries :: Python Modules",
32
+ ]
33
+
34
+ [project.urls]
35
+ Homepage = "https://worthune.com"
36
+ Documentation = "https://worthune.com/docs"
37
+ Repository = "https://github.com/CapsteraSupport/worthune-sdk"
38
+
39
+ [tool.hatch.build.targets.wheel]
40
+ packages = ["src/worthune"]
@@ -0,0 +1,206 @@
1
+ """Worthune SDK — thin, zero-dependency client for the Worthune Model API.
2
+
3
+ 27 financial calculation models, each with a published spec and an
4
+ independent second implementation that must agree with the first before
5
+ anything ships. Docs: https://worthune.com/docs
6
+
7
+ The SDK is a client only — the models, specs, and verification harness live
8
+ behind the API. Free with attribution: https://worthune.com/pricing
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import hashlib
14
+ import json
15
+ import urllib.error
16
+ import urllib.request
17
+ from decimal import Decimal
18
+ from typing import Any
19
+
20
+ __version__ = "0.1.0"
21
+ __all__ = [
22
+ "Worthune",
23
+ "WorthuneError",
24
+ "canonical_json",
25
+ "verify_record",
26
+ ]
27
+
28
+ _DEFAULT_BASE_URL = "https://worthune.com"
29
+ _USER_AGENT = f"worthune-python/{__version__}"
30
+
31
+
32
+ class WorthuneError(Exception):
33
+ """Raised for transport failures and non-validation HTTP errors."""
34
+
35
+ def __init__(self, message: str, status: int | None = None, body: Any = None):
36
+ super().__init__(message)
37
+ self.status = status
38
+ self.body = body
39
+
40
+
41
+ class Worthune:
42
+ """Client for the Worthune Model API.
43
+
44
+ >>> client = Worthune()
45
+ >>> result = client.run("relocation", {"currentSalary": 95000, ...})
46
+ >>> result["outputs"]["breakEvenMonths"]
47
+ """
48
+
49
+ def __init__(self, base_url: str = _DEFAULT_BASE_URL, timeout: float = 30.0):
50
+ self.base_url = base_url.rstrip("/")
51
+ self.timeout = timeout
52
+
53
+ # ── transport ────────────────────────────────────────────────────────────
54
+ def _request(self, path: str, payload: dict[str, Any] | None = None) -> bytes:
55
+ url = f"{self.base_url}{path}"
56
+ data = None
57
+ headers = {"user-agent": _USER_AGENT, "accept": "*/*"}
58
+ if payload is not None:
59
+ data = json.dumps(payload).encode("utf-8")
60
+ headers["content-type"] = "application/json"
61
+ req = urllib.request.Request(url, data=data, headers=headers)
62
+ try:
63
+ with urllib.request.urlopen(req, timeout=self.timeout) as res:
64
+ return res.read()
65
+ except urllib.error.HTTPError as err:
66
+ body_bytes = err.read()
67
+ # 400s carry the structured validation envelope callers want;
68
+ # everything else (404, 429 burst backstop, 5xx) raises.
69
+ if err.code == 400:
70
+ return body_bytes
71
+ body: Any = None
72
+ try:
73
+ body = json.loads(body_bytes)
74
+ except ValueError:
75
+ pass
76
+ raise WorthuneError(
77
+ f"{'POST' if payload is not None else 'GET'} {path} -> {err.code}",
78
+ status=err.code,
79
+ body=body,
80
+ ) from err
81
+ except urllib.error.URLError as err:
82
+ raise WorthuneError(f"request to {url} failed: {err.reason}") from err
83
+
84
+ def _get_json(self, path: str) -> Any:
85
+ return json.loads(self._request(path))
86
+
87
+ # ── API surface ──────────────────────────────────────────────────────────
88
+ def list_models(self) -> list[dict[str, Any]]:
89
+ """Catalog of all models with spec versions."""
90
+ return self._get_json("/api/v1/models")["models"]
91
+
92
+ def get_contract(self, model: str) -> dict[str, Any]:
93
+ """Machine-readable contract: inputs, domains, sentinels, spec version."""
94
+ return self._get_json(f"/api/v1/models/{model}")
95
+
96
+ def get_spec(self, model: str) -> str:
97
+ """The full published spec, as markdown."""
98
+ return self._request(f"/api/v1/models/{model}/spec").decode("utf-8")
99
+
100
+ def run(self, model: str, inputs: dict[str, Any]) -> dict[str, Any]:
101
+ """Run a model.
102
+
103
+ Returns the full envelope — outputs, sentinels, assumptions, cited
104
+ facts, and the decision record. Validation failures return
105
+ ``{"ok": False, "errors": [...]}`` rather than raising; network and
106
+ server errors raise :class:`WorthuneError`.
107
+ """
108
+ return json.loads(self._request(f"/api/v1/models/{model}", payload=inputs))
109
+
110
+ def list_evals(self) -> dict[str, Any]:
111
+ """Index of verified eval datasets."""
112
+ return self._get_json("/api/v1/evals")
113
+
114
+ def get_eval_dataset(self, model: str) -> dict[str, Any]:
115
+ """One eval dataset: ``{"meta": {...}, "cases": [...]}``.
116
+
117
+ 250 deterministic input/expected-output pairs — the exact cases
118
+ Worthune's dual-implementation CI harness verifies.
119
+ """
120
+ lines = self._request(f"/api/v1/evals/{model}").decode("utf-8").strip().split("\n")
121
+ return {
122
+ "meta": json.loads(lines[0]),
123
+ "cases": [json.loads(line) for line in lines[1:]],
124
+ }
125
+
126
+ def get_facts(self) -> list[dict[str, Any]]:
127
+ """The sourced-constants registry (IRS limits, brackets, SSA factors)."""
128
+ return self._get_json("/api/v1/facts")["facts"]
129
+
130
+
131
+ # ── Decision-record verification ─────────────────────────────────────────────
132
+ # Every successful run carries record.sha256 — SHA-256 over the canonical JSON
133
+ # of {model, specVersion, inputs, outputs} with object keys sorted recursively
134
+ # and numbers rendered exactly as ECMAScript renders them (the server is
135
+ # JavaScript). Recompute it any time to prove a stored response's numbers came
136
+ # from that spec version with those inputs, unaltered.
137
+
138
+
139
+ def _js_number_repr(value: float) -> str:
140
+ """Render a float exactly as ECMAScript Number::toString(10) would.
141
+
142
+ Python and JavaScript both choose shortest round-trip digits, but format
143
+ them differently (e.g. Python ``5e-05`` vs JS ``0.00005``; Python
144
+ ``1e+21`` matches JS, but Python switches to exponent form at different
145
+ thresholds). JSON numbers parsed from API responses hash correctly only
146
+ if re-rendered the way the server rendered them.
147
+ """
148
+ if value != value or value in (float("inf"), float("-inf")):
149
+ raise ValueError("non-finite numbers cannot appear in JSON")
150
+ if value == 0:
151
+ return "0" # covers -0.0, which JS renders as "0"
152
+ sign = "-" if value < 0 else ""
153
+ d = Decimal(repr(abs(value))).normalize()
154
+ digit_tuple, exp = d.as_tuple()[1], int(d.as_tuple()[2])
155
+ digits = "".join(str(x) for x in digit_tuple)
156
+ k = len(digits)
157
+ n = exp + k # value = 0.<digits> * 10^n
158
+ if k <= n <= 21:
159
+ return sign + digits + "0" * (n - k)
160
+ if 0 < n <= 21:
161
+ return sign + digits[:n] + "." + digits[n:]
162
+ if -6 < n <= 0:
163
+ return sign + "0." + "0" * (-n) + digits
164
+ # exponential form; JS writes e+21 / e-7 (no zero padding)
165
+ e = n - 1
166
+ e_str = f"e+{e}" if e >= 0 else f"e-{-e}"
167
+ if k == 1:
168
+ return sign + digits + e_str
169
+ return sign + digits[0] + "." + digits[1:] + e_str
170
+
171
+
172
+ def canonical_json(value: Any) -> str:
173
+ """Serialize with object keys sorted recursively, no whitespace,
174
+ JS-compatible number rendering."""
175
+ if isinstance(value, bool):
176
+ return "true" if value else "false"
177
+ if value is None:
178
+ return "null"
179
+ if isinstance(value, str):
180
+ return json.dumps(value, ensure_ascii=False)
181
+ if isinstance(value, int):
182
+ return str(value)
183
+ if isinstance(value, float):
184
+ return _js_number_repr(value)
185
+ if isinstance(value, (list, tuple)):
186
+ return "[" + ",".join(canonical_json(v) for v in value) + "]"
187
+ if isinstance(value, dict):
188
+ items = sorted(value.items(), key=lambda kv: kv[0])
189
+ return "{" + ",".join(
190
+ f"{json.dumps(k, ensure_ascii=False)}:{canonical_json(v)}" for k, v in items
191
+ ) + "}"
192
+ raise TypeError(f"unsupported type in canonical JSON: {type(value)!r}")
193
+
194
+
195
+ def verify_record(response: dict[str, Any]) -> bool:
196
+ """Recompute a response's decision-record hash and compare."""
197
+ canonical = canonical_json(
198
+ {
199
+ "model": response["model"],
200
+ "specVersion": response["specVersion"],
201
+ "inputs": response["inputs"],
202
+ "outputs": response["outputs"],
203
+ }
204
+ )
205
+ digest = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
206
+ return digest == response["record"]["sha256"]
@@ -0,0 +1 @@
1
+ {"ok":true,"model":"relocation","specVersion":"1.0.0","contractUrl":"/api/v1/models/relocation","specUrl":"/api/v1/models/relocation/spec","inputs":{"currentSalary":95000,"newSalary":108000,"currentMonthlyExpenses":4200,"newMonthlyExpenses":4900,"movingCosts":6000,"currentSavings":40000,"annualReturn":0.07,"yearsHorizon":10},"outputs":{"annualSalaryDelta":13000,"annualExpenseDelta":8400,"annualNetDelta":4600,"movingCostRecoveryMonths":16,"breakEvenMonths":16,"wealthAtHorizonStay":694899.6333646487,"wealthAtHorizonMove":746652.3858427971,"netWealthGain":51752.75247814844,"yearlyData":[{"year":0,"stay":40000,"move":34000},{"year":1,"stay":87400,"move":85580},{"year":2,"stay":138118,"move":140770.6},{"year":3,"stay":192386.26,"move":199824.54200000002},{"year":4,"stay":250453.29820000002,"move":263012.25994},{"year":5,"stay":312585.029074,"move":330623.11813580006},{"year":6,"stay":379065.98110918,"move":402966.7364053061},{"year":7,"stay":450200.5997868226,"move":480374.4079536775},{"year":8,"stay":526314.6417719002,"move":563200.616510435},{"year":9,"stay":607756.6666959333,"move":651824.6596661655},{"year":10,"stay":694899.6333646487,"move":746652.3858427971}]},"sentinels":[{"field":"breakEvenMonths","value":9999,"meaning":"never breaks even (monthlyNetDelta ≤ 0)","triggered":false}],"assumptions":["Implements model spec relocation v1.0.0 (dual-implementation verified)."],"facts":[],"record":{"sha256":"7891f5eac540c8e9252f832de1742778a0e54b8812d3e198fe289e1c46b6bfd9","fields":["model","specVersion","inputs","outputs"],"howToVerify":"Store this record with any advice or agent output built on these numbers. To verify later: build {model, specVersion, inputs, outputs} from the stored response, serialize as JSON with object keys sorted recursively (no whitespace), and SHA-256 it — a match proves the numbers came from this spec version with these inputs, unaltered."},"disclaimer":"Illustrative planning model, not financial advice. Outputs follow the published model spec exactly; read the spec for assumptions and exclusions before relying on any number."}
@@ -0,0 +1,71 @@
1
+ """Offline tests: decision-record verification against a real captured
2
+ response, and JS-exact number rendering (ground truth generated with Node)."""
3
+
4
+ import copy
5
+ import json
6
+ import pathlib
7
+ import sys
8
+ import unittest
9
+
10
+ sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "src"))
11
+
12
+ from worthune import _js_number_repr, canonical_json, verify_record # noqa: E402
13
+
14
+ FIXTURE = json.loads(
15
+ (pathlib.Path(__file__).parent / "fixture-relocation.json").read_text()
16
+ )
17
+
18
+ # Expected strings generated with `node -e 'console.log(String(v))'`.
19
+ JS_NUMBER_CASES = [
20
+ (0.07, "0.07"),
21
+ (4600.0, "4600"),
22
+ (32409.87, "32409.87"),
23
+ (0.00005, "0.00005"),
24
+ (5e-7, "5e-7"),
25
+ (1e21, "1e+21"),
26
+ (1.5e21, "1.5e+21"),
27
+ (123.456, "123.456"),
28
+ (100.0, "100"),
29
+ (-0.0, "0"),
30
+ (1e-6, "0.000001"),
31
+ (9007199254740991.0, "9007199254740991"),
32
+ (2.5e-8, "2.5e-8"),
33
+ (-42.75, "-42.75"),
34
+ (0.1, "0.1"),
35
+ (3e20, "300000000000000000000"),
36
+ ]
37
+
38
+
39
+ class TestDecisionRecord(unittest.TestCase):
40
+ def test_fixture_verifies(self):
41
+ self.assertTrue(FIXTURE["ok"])
42
+ self.assertTrue(verify_record(FIXTURE))
43
+
44
+ def test_tampering_detected(self):
45
+ tampered = copy.deepcopy(FIXTURE)
46
+ tampered["outputs"]["breakEvenMonths"] = 1
47
+ self.assertFalse(verify_record(tampered))
48
+
49
+ def test_key_order_independent(self):
50
+ reordered = copy.deepcopy(FIXTURE)
51
+ reordered["inputs"] = dict(reversed(list(reordered["inputs"].items())))
52
+ self.assertTrue(verify_record(reordered))
53
+
54
+
55
+ class TestJsNumberRepr(unittest.TestCase):
56
+ def test_matches_ecmascript(self):
57
+ for value, expected in JS_NUMBER_CASES:
58
+ self.assertEqual(_js_number_repr(value), expected, msg=repr(value))
59
+
60
+ def test_ints_pass_through_canonical_json(self):
61
+ self.assertEqual(canonical_json({"a": 4600, "b": True, "c": None}), '{"a":4600,"b":true,"c":null}')
62
+
63
+ def test_canonical_sorts_recursively(self):
64
+ self.assertEqual(
65
+ canonical_json({"b": 1, "a": {"d": [2, {"z": 3, "y": 4}], "c": 5}}),
66
+ '{"a":{"c":5,"d":[2,{"y":4,"z":3}]},"b":1}',
67
+ )
68
+
69
+
70
+ if __name__ == "__main__":
71
+ unittest.main()