balanceproof 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,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: balanceproof
3
+ Version: 0.1.0
4
+ Summary: SEC fundamentals that prove they add up: checked balance sheets, income statements and cash flow, point-in-time.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://balanceproof.dev
7
+ Project-URL: Documentation, https://balanceproof.dev/api
8
+ Keywords: sec,edgar,xbrl,fundamentals,balance sheet,backtesting,point-in-time
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Intended Audience :: Financial and Insurance Industry
11
+ Classifier: Topic :: Office/Business :: Financial :: Investment
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ Provides-Extra: pandas
15
+ Requires-Dist: pandas>=1.5; extra == "pandas"
16
+
17
+ # balanceproof
18
+
19
+ Python client for [BalanceProof](https://balanceproof.dev): US public-company
20
+ fundamentals from SEC EDGAR, where every balance sheet, income statement and
21
+ cash flow is checked against its own totals, and a filing that does not add up
22
+ is flagged with the reason instead of passed through.
23
+
24
+ ```bash
25
+ pip install "balanceproof[pandas]"
26
+ ```
27
+
28
+ ```python
29
+ import balanceproof as bp
30
+
31
+ client = bp.Client("YOUR_KEY") # free key: https://balanceproof.dev/dashboard
32
+ # or set BALANCEPROOF_API_KEY
33
+
34
+ client.balance_sheet("AAPL")
35
+ client.statements("AAPL", period="quarterly")
36
+
37
+ # Point-in-time (Pro and above): only what had been filed by that date,
38
+ # so a backtest never sees a figure before it was public.
39
+ client.balance_sheet("AAPL", as_of="2025-03-01")
40
+
41
+ # A pandas panel, one row per company and period.
42
+ df = client.panel(["AAPL", "MSFT", "KO"], ["revenue", "net_income", "operating_cash_flow"],
43
+ period="quarterly")
44
+ clean = df[df.checks_failed.str.len() == 0] # drop periods that failed a check
45
+
46
+ # Failed checks and restatements across every company (Pro: 90 days, Business: all).
47
+ feed = client.exceptions(type="restatement")
48
+ client.to_frame(feed)
49
+ ```
50
+
51
+ Derived figures (Q4 as the year minus nine months, Q2/Q3 cash flow from
52
+ year-to-date filings) are listed in each period's `derived`.
53
+
54
+ A call that needs a higher plan raises `balanceproof.PlanRequired`, whose
55
+ `required_plan` names it.
@@ -0,0 +1,39 @@
1
+ # balanceproof
2
+
3
+ Python client for [BalanceProof](https://balanceproof.dev): US public-company
4
+ fundamentals from SEC EDGAR, where every balance sheet, income statement and
5
+ cash flow is checked against its own totals, and a filing that does not add up
6
+ is flagged with the reason instead of passed through.
7
+
8
+ ```bash
9
+ pip install "balanceproof[pandas]"
10
+ ```
11
+
12
+ ```python
13
+ import balanceproof as bp
14
+
15
+ client = bp.Client("YOUR_KEY") # free key: https://balanceproof.dev/dashboard
16
+ # or set BALANCEPROOF_API_KEY
17
+
18
+ client.balance_sheet("AAPL")
19
+ client.statements("AAPL", period="quarterly")
20
+
21
+ # Point-in-time (Pro and above): only what had been filed by that date,
22
+ # so a backtest never sees a figure before it was public.
23
+ client.balance_sheet("AAPL", as_of="2025-03-01")
24
+
25
+ # A pandas panel, one row per company and period.
26
+ df = client.panel(["AAPL", "MSFT", "KO"], ["revenue", "net_income", "operating_cash_flow"],
27
+ period="quarterly")
28
+ clean = df[df.checks_failed.str.len() == 0] # drop periods that failed a check
29
+
30
+ # Failed checks and restatements across every company (Pro: 90 days, Business: all).
31
+ feed = client.exceptions(type="restatement")
32
+ client.to_frame(feed)
33
+ ```
34
+
35
+ Derived figures (Q4 as the year minus nine months, Q2/Q3 cash flow from
36
+ year-to-date filings) are listed in each period's `derived`.
37
+
38
+ A call that needs a higher plan raises `balanceproof.PlanRequired`, whose
39
+ `required_plan` names it.
@@ -0,0 +1,193 @@
1
+ """BalanceProof: SEC fundamentals that prove they add up.
2
+
3
+ >>> import balanceproof as bp
4
+ >>> client = bp.Client("YOUR_KEY") # or set BALANCEPROOF_API_KEY
5
+ >>> client.balance_sheet("AAPL")
6
+ >>> client.statements("AAPL", period="quarterly")
7
+ >>> client.balance_sheet("AAPL", as_of="2025-03-01") # point-in-time (Pro+)
8
+ >>> client.panel(["AAPL", "MSFT"], ["revenue", "net_income"]) # pandas
9
+
10
+ Standard library only; pandas is needed just for `panel()` and `to_frame()`.
11
+ A free key is at https://balanceproof.dev/dashboard.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import datetime as _dt
17
+ import json
18
+ import os
19
+ import urllib.error
20
+ import urllib.parse
21
+ import urllib.request
22
+ from typing import Any, Iterable
23
+
24
+ __version__ = "0.1.0"
25
+ __all__ = ["Client", "BalanceProofError", "PlanRequired"]
26
+
27
+ DEFAULT_BASE_URL = "https://balanceproof.dev"
28
+
29
+
30
+ class BalanceProofError(Exception):
31
+ """The API answered with an error. `status` is the HTTP code."""
32
+
33
+ def __init__(self, status: int, detail: Any):
34
+ self.status = status
35
+ self.detail = detail
36
+ super().__init__(f"{status}: {detail}")
37
+
38
+
39
+ class PlanRequired(BalanceProofError):
40
+ """The feature needs a higher plan; `required_plan` names it."""
41
+
42
+ @property
43
+ def required_plan(self) -> str | None:
44
+ return self.detail.get("required_plan") if isinstance(self.detail, dict) else None
45
+
46
+
47
+ def _date(v: Any) -> str | None:
48
+ if v is None:
49
+ return None
50
+ if isinstance(v, (_dt.date, _dt.datetime)):
51
+ return v.isoformat()[:10]
52
+ return str(v)[:10]
53
+
54
+
55
+ class Client:
56
+ """One API key against one base URL. Every method is one metered call."""
57
+
58
+ def __init__(self, api_key: str | None = None, *, base_url: str | None = None,
59
+ timeout: float = 60.0):
60
+ self.api_key = api_key or os.environ.get("BALANCEPROOF_API_KEY", "")
61
+ self.base_url = (base_url or os.environ.get("BALANCEPROOF_BASE_URL")
62
+ or DEFAULT_BASE_URL).rstrip("/")
63
+ self.timeout = timeout
64
+
65
+ # -- transport ------------------------------------------------------------
66
+
67
+ def _request(self, method: str, path: str, params: dict[str, Any] | None = None,
68
+ body: Any = None, raw: bool = False) -> Any:
69
+ query = {k: v for k, v in (params or {}).items() if v is not None}
70
+ url = self.base_url + path + ("?" + urllib.parse.urlencode(query) if query else "")
71
+ headers = {"Accept": "application/json",
72
+ "User-Agent": f"balanceproof-python/{__version__}"}
73
+ if self.api_key:
74
+ headers["X-API-Key"] = self.api_key
75
+ data = None
76
+ if body is not None:
77
+ data = json.dumps(body).encode()
78
+ headers["Content-Type"] = "application/json"
79
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
80
+ try:
81
+ with urllib.request.urlopen(req, timeout=self.timeout) as res:
82
+ payload = res.read()
83
+ except urllib.error.HTTPError as exc:
84
+ text = exc.read().decode("utf-8", "replace")
85
+ try:
86
+ detail = json.loads(text).get("detail", text)
87
+ except ValueError:
88
+ detail = text
89
+ if exc.code == 403 and isinstance(detail, dict) and detail.get("error") == "plan_required":
90
+ raise PlanRequired(exc.code, detail) from None
91
+ raise BalanceProofError(exc.code, detail) from None
92
+ if raw:
93
+ return payload.decode("utf-8")
94
+ return json.loads(payload)
95
+
96
+ # -- endpoints ------------------------------------------------------------
97
+
98
+ def balance_sheet(self, ticker: str, as_of: Any = None) -> dict[str, Any]:
99
+ """The latest filed balance sheet, or as public on `as_of` (Pro+)."""
100
+ return self._request("GET", f"/api/company/{urllib.parse.quote(ticker)}",
101
+ {"as_of": _date(as_of)})
102
+
103
+ def history(self, ticker: str, years: int | None = None, as_of: Any = None) -> dict[str, Any]:
104
+ """Every filed balance sheet, newest first. Depth is capped by plan."""
105
+ return self._request("GET", f"/api/company/{urllib.parse.quote(ticker)}/history",
106
+ {"years": years, "as_of": _date(as_of)})
107
+
108
+ def statements(self, ticker: str, period: str = "annual", years: int | None = None,
109
+ as_of: Any = None) -> dict[str, Any]:
110
+ """Income statement and cash flow per period, each with its checks."""
111
+ return self._request("GET", f"/api/company/{urllib.parse.quote(ticker)}/statements",
112
+ {"period": period, "years": years, "as_of": _date(as_of)})
113
+
114
+ def changes(self, ticker: str) -> dict[str, Any]:
115
+ """What moved since the previous period, and what was restated (Starter+)."""
116
+ return self._request("GET", f"/api/company/{urllib.parse.quote(ticker)}/changes")
117
+
118
+ def verify(self, tickers: Iterable[str]) -> dict[str, Any]:
119
+ """Check many balance sheets at once (Pro: 50, Business: 500)."""
120
+ return self._request("POST", "/api/verify", body={"tickers": list(tickers)})
121
+
122
+ def exceptions(self, since: Any = None, until: Any = None, type: str | None = None, # noqa: A002
123
+ ticker: str | None = None, filer_only: bool = False,
124
+ limit: int = 500, offset: int = 0) -> dict[str, Any]:
125
+ """Failed checks and restatements across all companies (Pro: 90 days)."""
126
+ params = {"since": _date(since), "until": _date(until), "type": type,
127
+ "ticker": ticker, "limit": limit, "offset": offset}
128
+ if filer_only:
129
+ params.update(type="failed_check", attribution="filer")
130
+ return self._request("GET", "/api/exceptions", params)
131
+
132
+ def exceptions_csv(self, **kwargs: Any) -> str:
133
+ """The same feed as CSV text."""
134
+ params = {"since": _date(kwargs.get("since")), "until": _date(kwargs.get("until")),
135
+ "type": kwargs.get("type"), "ticker": kwargs.get("ticker"),
136
+ "limit": kwargs.get("limit", 5000), "format": "csv"}
137
+ return self._request("GET", "/api/exceptions", params, raw=True)
138
+
139
+ def status(self) -> dict[str, Any]:
140
+ """Your plan and how many calls are left this month. Free of charge."""
141
+ return self._request("GET", "/api/user/status")
142
+
143
+ # -- pandas ---------------------------------------------------------------
144
+
145
+ def panel(self, tickers: Iterable[str], fields: Iterable[str] | None = None,
146
+ period: str = "annual", years: int | None = None, as_of: Any = None):
147
+ """A long pandas DataFrame: one row per (ticker, period), one column per field.
148
+
149
+ `fields` are statement metrics ("revenue", "net_income",
150
+ "operating_cash_flow", ...); None keeps them all. Each row also carries
151
+ `filing_date`, `fiscal_period`, `checks_failed` and `derived`, so a
152
+ backtest can drop derived or failed rows. One call per ticker.
153
+ """
154
+ pd = _pandas()
155
+ wanted = list(fields) if fields is not None else None
156
+ rows: list[dict[str, Any]] = []
157
+ for t in tickers:
158
+ try:
159
+ body = self.statements(t, period=period, years=years, as_of=as_of)
160
+ except BalanceProofError as exc:
161
+ if exc.status == 404:
162
+ continue
163
+ raise
164
+ for p in body["periods"]:
165
+ values = {**p["income_statement"], **p["cash_flow"]}
166
+ row = {"ticker": body["ticker"], "period_end": p["period_end"],
167
+ "fiscal_period": p["fiscal_period"], "filing_date": p["filing_date"]}
168
+ row.update({k: v for k, v in values.items() if wanted is None or k in wanted})
169
+ row["checks_failed"] = [c["check"] for c in p["checks"] if c["status"] == "failed"]
170
+ row["derived"] = p["derived"]
171
+ rows.append(row)
172
+ df = pd.DataFrame(rows)
173
+ for col in ("period_end", "filing_date"):
174
+ if col in df:
175
+ df[col] = pd.to_datetime(df[col])
176
+ return df
177
+
178
+ @staticmethod
179
+ def to_frame(payload: dict[str, Any]):
180
+ """Any list-bearing response (history, statements, exceptions) as a DataFrame."""
181
+ pd = _pandas()
182
+ for key in ("events", "periods", "balance_sheets", "results"):
183
+ if key in payload:
184
+ return pd.json_normalize(payload[key])
185
+ return pd.json_normalize(payload)
186
+
187
+
188
+ def _pandas():
189
+ try:
190
+ import pandas as pd
191
+ except ImportError as exc: # pragma: no cover - depends on the environment
192
+ raise ImportError("panel() and to_frame() need pandas: pip install pandas") from exc
193
+ return pd
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: balanceproof
3
+ Version: 0.1.0
4
+ Summary: SEC fundamentals that prove they add up: checked balance sheets, income statements and cash flow, point-in-time.
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://balanceproof.dev
7
+ Project-URL: Documentation, https://balanceproof.dev/api
8
+ Keywords: sec,edgar,xbrl,fundamentals,balance sheet,backtesting,point-in-time
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Intended Audience :: Financial and Insurance Industry
11
+ Classifier: Topic :: Office/Business :: Financial :: Investment
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ Provides-Extra: pandas
15
+ Requires-Dist: pandas>=1.5; extra == "pandas"
16
+
17
+ # balanceproof
18
+
19
+ Python client for [BalanceProof](https://balanceproof.dev): US public-company
20
+ fundamentals from SEC EDGAR, where every balance sheet, income statement and
21
+ cash flow is checked against its own totals, and a filing that does not add up
22
+ is flagged with the reason instead of passed through.
23
+
24
+ ```bash
25
+ pip install "balanceproof[pandas]"
26
+ ```
27
+
28
+ ```python
29
+ import balanceproof as bp
30
+
31
+ client = bp.Client("YOUR_KEY") # free key: https://balanceproof.dev/dashboard
32
+ # or set BALANCEPROOF_API_KEY
33
+
34
+ client.balance_sheet("AAPL")
35
+ client.statements("AAPL", period="quarterly")
36
+
37
+ # Point-in-time (Pro and above): only what had been filed by that date,
38
+ # so a backtest never sees a figure before it was public.
39
+ client.balance_sheet("AAPL", as_of="2025-03-01")
40
+
41
+ # A pandas panel, one row per company and period.
42
+ df = client.panel(["AAPL", "MSFT", "KO"], ["revenue", "net_income", "operating_cash_flow"],
43
+ period="quarterly")
44
+ clean = df[df.checks_failed.str.len() == 0] # drop periods that failed a check
45
+
46
+ # Failed checks and restatements across every company (Pro: 90 days, Business: all).
47
+ feed = client.exceptions(type="restatement")
48
+ client.to_frame(feed)
49
+ ```
50
+
51
+ Derived figures (Q4 as the year minus nine months, Q2/Q3 cash flow from
52
+ year-to-date filings) are listed in each period's `derived`.
53
+
54
+ A call that needs a higher plan raises `balanceproof.PlanRequired`, whose
55
+ `required_plan` names it.
@@ -0,0 +1,8 @@
1
+ README.md
2
+ pyproject.toml
3
+ balanceproof/__init__.py
4
+ balanceproof.egg-info/PKG-INFO
5
+ balanceproof.egg-info/SOURCES.txt
6
+ balanceproof.egg-info/dependency_links.txt
7
+ balanceproof.egg-info/requires.txt
8
+ balanceproof.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+
2
+ [pandas]
3
+ pandas>=1.5
@@ -0,0 +1 @@
1
+ balanceproof
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "balanceproof"
7
+ version = "0.1.0"
8
+ description = "SEC fundamentals that prove they add up: checked balance sheets, income statements and cash flow, point-in-time."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ dependencies = []
13
+ keywords = ["sec", "edgar", "xbrl", "fundamentals", "balance sheet", "backtesting", "point-in-time"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Intended Audience :: Financial and Insurance Industry",
17
+ "Topic :: Office/Business :: Financial :: Investment",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ pandas = ["pandas>=1.5"]
22
+
23
+ [project.urls]
24
+ Homepage = "https://balanceproof.dev"
25
+ Documentation = "https://balanceproof.dev/api"
26
+
27
+ [tool.setuptools]
28
+ packages = ["balanceproof"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+