beatandraise 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,6 @@
1
+ __pycache__/
2
+ *.pyc
3
+ dist/
4
+ build/
5
+ *.egg-info/
6
+ .venv/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Iterative Works GmbH
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.5
2
+ Name: beatandraise
3
+ Version: 0.1.0
4
+ Summary: Python client for the beatandraise.com SEC Filings API: the whole SEC EDGAR archive, 10-K/10-Q/8-K Items, XBRL financial statements, Form 4 insider trades and 13F holders.
5
+ Project-URL: Homepage, https://beatandraise.com/sec-filings-api
6
+ Project-URL: Documentation, https://beatandraise.com/sec-filings-api/docs
7
+ Project-URL: Guides, https://beatandraise.com/sec-filings-api/guides
8
+ Project-URL: API reference (OpenAPI), https://beatandraise.com/api/v1/openapi.json
9
+ Project-URL: Source, https://github.com/mchennupati/beatandraise-python
10
+ Author-email: Iterative Works GmbH <support@beatandraise.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: 10-k,10-q,13f,8-k,edgar,financial-statements,form-4,insider-trading,llm,sec,sec-api,sec-filings,xbrl
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Financial and Insurance Industry
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Topic :: Office/Business :: Financial :: Investment
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+
23
+ # beatandraise: Python client for the SEC Filings API
24
+
25
+ Python client for the [beatandraise.com SEC Filings API](https://beatandraise.com/sec-filings-api), an SEC EDGAR API designed for LLMs. It covers:
26
+
27
+ - **The whole EDGAR archive.** Every filing since 1993, about 18.8 million, new filings within minutes, with every document as text.
28
+ - **10-K, 10-Q, 8-K and 20-F Items.** Risk Factors, MD&A, or any 8-K Item, found by its heading.
29
+ - **XBRL financial statements.** As the company presented them, with each line's tag and a link to the figure.
30
+ - **Ownership data.** Form 4 insider transactions, 13F institutional holders and portfolios, and shares outstanding with splits.
31
+ - **Every SEC XML form as JSON.** Form D, 144, 13D/G, N-PX and others.
32
+ - **Verification.** Checks for quotes, figures and citations against the filing.
33
+
34
+ Every result names the filing it came from. The package uses only the Python standard library and needs Python 3.9 or later.
35
+
36
+ ```bash
37
+ pip install beatandraise
38
+ export BEATANDRAISE_API_KEY=bar_... # free key: https://beatandraise.com/api-keys
39
+ ```
40
+
41
+ ## Quickstart
42
+
43
+ ```python
44
+ from beatandraise import Client
45
+
46
+ api = Client() # reads BEATANDRAISE_API_KEY
47
+
48
+ # Find a company and its latest 10-K
49
+ apple = api.search_companies("AAPL")
50
+ tenk = api.filings(cik="0000320193", form="10-K", limit=1)["filings"][0]
51
+ acc = tenk["accession_number"]
52
+
53
+ # Risk Factors (Item 1A) as text: the section itself, not the table of contents
54
+ risk_factors = api.item(acc, "1A")
55
+
56
+ # Income statement across the last five 10-Ks
57
+ history = api.financial_history(cik="0000320193", period="annual", statements="income", limit=5)
58
+
59
+ # Who owns it: % held by 13F filers, top holders, buyers and sellers
60
+ holders = api.institutional_holders("AAPL")
61
+ q = holders["quarters"][0]
62
+ print(q["period"], q["share_classes"][0]["pct_of_class"], "% held by", q["holders"], "13F filers")
63
+ ```
64
+
65
+ ## Examples
66
+
67
+ ```python
68
+ # Insider sales and purchases, with the Rule 10b5-1 flag and footnotes
69
+ rows = api.insider_transactions(cik="0001045810", from_="2026-09-01", include_footnotes=True)["transactions"]
70
+ sales = [r for r in rows if r["transaction_code"] == "S"]
71
+
72
+ # 8-K officer changes (Item 5.02) across all companies, with the Item's text
73
+ events = api.filings(item="5.02", item_text="5.02", from_="2026-11-01", limit=25)
74
+
75
+ # Full-text search, one result per company
76
+ hits = api.search('"going concern"', form="10-K", year=2026, by_company=True)
77
+
78
+ # Lines in a filing that mention a term
79
+ lines = api.search_document("0000320193", acc, q="tariffs")
80
+
81
+ # Statement lines with XBRL concept, value in full units and a highlight link
82
+ stmts = api.statements("0000320193", acc, include_tags=True)
83
+
84
+ # Check a figure before you quote it
85
+ check = api.verify_figure("0000320193", acc, value="$478.8 billion", label="Net sales")
86
+
87
+ # Any endpoint by path
88
+ api.get("/fiscal-calendar", cik="0000320193")
89
+ ```
90
+
91
+ Parameters named `from` in the API are `from_` in Python. Lists are sent comma-separated (`statements=["income", "cash_flow"]`).
92
+
93
+ ## Methods
94
+
95
+ | Method | Endpoint |
96
+ |---|---|
97
+ | `filings(cik, form, from_, to, item, item_text, limit, offset)` | `GET /filings` |
98
+ | `search(q, form, cik, year, limit, page, by_company)` | `GET /search` |
99
+ | `search_companies(q)`, `company(cik)` | `GET /company/search`, `/company/{cik}` |
100
+ | `identifiers(ticker, cik, cusip)` | `GET /identifiers` |
101
+ | `sic(...)` | `GET /sic` |
102
+ | `fiscal_calendar(cik, fiscal_year, from_, to)` | `GET /fiscal-calendar` |
103
+ | `filing(acc)`, `filing_tables(acc)`, `files(acc)` | `GET /filing/{acc}`, `/filing/{acc}/tables`, `/files/{acc}` |
104
+ | `download(acc, filename)` (bytes) | `GET /files/{acc}/{filename}` |
105
+ | `document(cik, acc, filename, markdown=True)` (text) | `GET /document/{cik}/{acc}/{filename}` |
106
+ | `search_document(cik, acc, q, filename, context)` | `GET /document/search/...` |
107
+ | `items(acc)`, `item(acc, item)` (text) | `GET /items/{acc}`, `/items/{acc}/{item}` |
108
+ | `form_data(cik, form, ...)` | `GET /form-data` |
109
+ | `statements(cik, acc, include_tags)` | `GET /statements/{cik}/{acc}` |
110
+ | `financial_history(cik, period, statements, limit, fiscal_year, fiscal_period, include_tags)` | `GET /financial-history` |
111
+ | `insider_transactions(cik, from_, to, include_table_ii, include_footnotes)` | `GET /insider-transactions` |
112
+ | `shares_outstanding(cik, share_class, from_, to)` | `GET /shares-outstanding` |
113
+ | `holdings(cik, period)` | `GET /holdings/{cik}` |
114
+ | `institutional_holders(company, quarters, holder, detail)` | `GET /institutional-holders/{company}` |
115
+ | `verify_quote(cik, acc, filename, quote)` | `GET /document/verify/...` |
116
+ | `verify_figure(cik, acc, value, label, filename)` | `GET /document/verify-figure/...` |
117
+ | `figures(cik, acc, filename, near)` | `GET /document/figures/...` |
118
+ | `check_citations(text)` | `POST /check-citations` |
119
+ | `account()` | `GET /account` |
120
+
121
+ The full reference, with an example response for each endpoint, is the [OpenAPI spec](https://beatandraise.com/api/v1/openapi.json). You can also [try any request in the browser](https://beatandraise.com/sec-filings-api/try).
122
+
123
+ ## Errors and limits
124
+
125
+ A refused request raises `beatandraise.APIError` with `.status` and `.body`. Requests refused for rate (429) or by a busy server (502-504) are retried twice, honouring `Retry-After`. The free plan includes 100 successful requests at up to 10 a second; Pro is unlimited at 20 a second. See [plans](https://beatandraise.com/sec-filings-api#plans).
126
+
127
+ ## Using it from an LLM
128
+
129
+ To give Claude, ChatGPT, Cursor or Codex the same data with no code, add the MCP server at `https://mcp.beatandraise.com/mcp`. Setup for each client is at [beatandraise.com/mcp](https://beatandraise.com/mcp).
130
+
131
+ ## Guides
132
+
133
+ - [Who owns a stock: 13F institutional holders by API](https://beatandraise.com/sec-filings-api/guides/13f-institutional-holders-api)
134
+ - [Get a 10-K's Risk Factors (Item 1A) by API](https://beatandraise.com/sec-filings-api/guides/10-k-risk-factors-api)
135
+ - [Insider trading data by API: Form 4 transactions as filed](https://beatandraise.com/sec-filings-api/guides/insider-trading-form-4-api)
136
+ - [Financial statements from SEC XBRL by API](https://beatandraise.com/sec-filings-api/guides/xbrl-financial-statements-api)
137
+
138
+ Questions: support@beatandraise.com. Data source: SEC EDGAR. Nothing here is investment advice.
@@ -0,0 +1,116 @@
1
+ # beatandraise: Python client for the SEC Filings API
2
+
3
+ Python client for the [beatandraise.com SEC Filings API](https://beatandraise.com/sec-filings-api), an SEC EDGAR API designed for LLMs. It covers:
4
+
5
+ - **The whole EDGAR archive.** Every filing since 1993, about 18.8 million, new filings within minutes, with every document as text.
6
+ - **10-K, 10-Q, 8-K and 20-F Items.** Risk Factors, MD&A, or any 8-K Item, found by its heading.
7
+ - **XBRL financial statements.** As the company presented them, with each line's tag and a link to the figure.
8
+ - **Ownership data.** Form 4 insider transactions, 13F institutional holders and portfolios, and shares outstanding with splits.
9
+ - **Every SEC XML form as JSON.** Form D, 144, 13D/G, N-PX and others.
10
+ - **Verification.** Checks for quotes, figures and citations against the filing.
11
+
12
+ Every result names the filing it came from. The package uses only the Python standard library and needs Python 3.9 or later.
13
+
14
+ ```bash
15
+ pip install beatandraise
16
+ export BEATANDRAISE_API_KEY=bar_... # free key: https://beatandraise.com/api-keys
17
+ ```
18
+
19
+ ## Quickstart
20
+
21
+ ```python
22
+ from beatandraise import Client
23
+
24
+ api = Client() # reads BEATANDRAISE_API_KEY
25
+
26
+ # Find a company and its latest 10-K
27
+ apple = api.search_companies("AAPL")
28
+ tenk = api.filings(cik="0000320193", form="10-K", limit=1)["filings"][0]
29
+ acc = tenk["accession_number"]
30
+
31
+ # Risk Factors (Item 1A) as text: the section itself, not the table of contents
32
+ risk_factors = api.item(acc, "1A")
33
+
34
+ # Income statement across the last five 10-Ks
35
+ history = api.financial_history(cik="0000320193", period="annual", statements="income", limit=5)
36
+
37
+ # Who owns it: % held by 13F filers, top holders, buyers and sellers
38
+ holders = api.institutional_holders("AAPL")
39
+ q = holders["quarters"][0]
40
+ print(q["period"], q["share_classes"][0]["pct_of_class"], "% held by", q["holders"], "13F filers")
41
+ ```
42
+
43
+ ## Examples
44
+
45
+ ```python
46
+ # Insider sales and purchases, with the Rule 10b5-1 flag and footnotes
47
+ rows = api.insider_transactions(cik="0001045810", from_="2026-09-01", include_footnotes=True)["transactions"]
48
+ sales = [r for r in rows if r["transaction_code"] == "S"]
49
+
50
+ # 8-K officer changes (Item 5.02) across all companies, with the Item's text
51
+ events = api.filings(item="5.02", item_text="5.02", from_="2026-11-01", limit=25)
52
+
53
+ # Full-text search, one result per company
54
+ hits = api.search('"going concern"', form="10-K", year=2026, by_company=True)
55
+
56
+ # Lines in a filing that mention a term
57
+ lines = api.search_document("0000320193", acc, q="tariffs")
58
+
59
+ # Statement lines with XBRL concept, value in full units and a highlight link
60
+ stmts = api.statements("0000320193", acc, include_tags=True)
61
+
62
+ # Check a figure before you quote it
63
+ check = api.verify_figure("0000320193", acc, value="$478.8 billion", label="Net sales")
64
+
65
+ # Any endpoint by path
66
+ api.get("/fiscal-calendar", cik="0000320193")
67
+ ```
68
+
69
+ Parameters named `from` in the API are `from_` in Python. Lists are sent comma-separated (`statements=["income", "cash_flow"]`).
70
+
71
+ ## Methods
72
+
73
+ | Method | Endpoint |
74
+ |---|---|
75
+ | `filings(cik, form, from_, to, item, item_text, limit, offset)` | `GET /filings` |
76
+ | `search(q, form, cik, year, limit, page, by_company)` | `GET /search` |
77
+ | `search_companies(q)`, `company(cik)` | `GET /company/search`, `/company/{cik}` |
78
+ | `identifiers(ticker, cik, cusip)` | `GET /identifiers` |
79
+ | `sic(...)` | `GET /sic` |
80
+ | `fiscal_calendar(cik, fiscal_year, from_, to)` | `GET /fiscal-calendar` |
81
+ | `filing(acc)`, `filing_tables(acc)`, `files(acc)` | `GET /filing/{acc}`, `/filing/{acc}/tables`, `/files/{acc}` |
82
+ | `download(acc, filename)` (bytes) | `GET /files/{acc}/{filename}` |
83
+ | `document(cik, acc, filename, markdown=True)` (text) | `GET /document/{cik}/{acc}/{filename}` |
84
+ | `search_document(cik, acc, q, filename, context)` | `GET /document/search/...` |
85
+ | `items(acc)`, `item(acc, item)` (text) | `GET /items/{acc}`, `/items/{acc}/{item}` |
86
+ | `form_data(cik, form, ...)` | `GET /form-data` |
87
+ | `statements(cik, acc, include_tags)` | `GET /statements/{cik}/{acc}` |
88
+ | `financial_history(cik, period, statements, limit, fiscal_year, fiscal_period, include_tags)` | `GET /financial-history` |
89
+ | `insider_transactions(cik, from_, to, include_table_ii, include_footnotes)` | `GET /insider-transactions` |
90
+ | `shares_outstanding(cik, share_class, from_, to)` | `GET /shares-outstanding` |
91
+ | `holdings(cik, period)` | `GET /holdings/{cik}` |
92
+ | `institutional_holders(company, quarters, holder, detail)` | `GET /institutional-holders/{company}` |
93
+ | `verify_quote(cik, acc, filename, quote)` | `GET /document/verify/...` |
94
+ | `verify_figure(cik, acc, value, label, filename)` | `GET /document/verify-figure/...` |
95
+ | `figures(cik, acc, filename, near)` | `GET /document/figures/...` |
96
+ | `check_citations(text)` | `POST /check-citations` |
97
+ | `account()` | `GET /account` |
98
+
99
+ The full reference, with an example response for each endpoint, is the [OpenAPI spec](https://beatandraise.com/api/v1/openapi.json). You can also [try any request in the browser](https://beatandraise.com/sec-filings-api/try).
100
+
101
+ ## Errors and limits
102
+
103
+ A refused request raises `beatandraise.APIError` with `.status` and `.body`. Requests refused for rate (429) or by a busy server (502-504) are retried twice, honouring `Retry-After`. The free plan includes 100 successful requests at up to 10 a second; Pro is unlimited at 20 a second. See [plans](https://beatandraise.com/sec-filings-api#plans).
104
+
105
+ ## Using it from an LLM
106
+
107
+ To give Claude, ChatGPT, Cursor or Codex the same data with no code, add the MCP server at `https://mcp.beatandraise.com/mcp`. Setup for each client is at [beatandraise.com/mcp](https://beatandraise.com/mcp).
108
+
109
+ ## Guides
110
+
111
+ - [Who owns a stock: 13F institutional holders by API](https://beatandraise.com/sec-filings-api/guides/13f-institutional-holders-api)
112
+ - [Get a 10-K's Risk Factors (Item 1A) by API](https://beatandraise.com/sec-filings-api/guides/10-k-risk-factors-api)
113
+ - [Insider trading data by API: Form 4 transactions as filed](https://beatandraise.com/sec-filings-api/guides/insider-trading-form-4-api)
114
+ - [Financial statements from SEC XBRL by API](https://beatandraise.com/sec-filings-api/guides/xbrl-financial-statements-api)
115
+
116
+ Questions: support@beatandraise.com. Data source: SEC EDGAR. Nothing here is investment advice.
@@ -0,0 +1,33 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "beatandraise"
7
+ version = "0.1.0"
8
+ description = "Python client for the beatandraise.com SEC Filings API: the whole SEC EDGAR archive, 10-K/10-Q/8-K Items, XBRL financial statements, Form 4 insider trades and 13F holders."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Iterative Works GmbH", email = "support@beatandraise.com" }]
14
+ keywords = ["sec", "edgar", "sec-api", "sec-filings", "10-k", "10-q", "8-k", "xbrl", "13f", "form-4", "insider-trading", "financial-statements", "llm"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "Intended Audience :: Financial and Insurance Industry",
19
+ "Programming Language :: Python :: 3",
20
+ "Topic :: Office/Business :: Financial :: Investment",
21
+ "Operating System :: OS Independent",
22
+ ]
23
+ dependencies = []
24
+
25
+ [project.urls]
26
+ Homepage = "https://beatandraise.com/sec-filings-api"
27
+ Documentation = "https://beatandraise.com/sec-filings-api/docs"
28
+ Guides = "https://beatandraise.com/sec-filings-api/guides"
29
+ "API reference (OpenAPI)" = "https://beatandraise.com/api/v1/openapi.json"
30
+ Source = "https://github.com/mchennupati/beatandraise-python"
31
+
32
+ [tool.hatch.build.targets.wheel]
33
+ packages = ["src/beatandraise"]
@@ -0,0 +1,7 @@
1
+ """beatandraise: Python client for the beatandraise.com SEC Filings API."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ from .client import Client, APIError, DEFAULT_BASE_URL # noqa: E402
6
+
7
+ __all__ = ["Client", "APIError", "DEFAULT_BASE_URL", "__version__"]
@@ -0,0 +1,281 @@
1
+ """Client for the beatandraise.com SEC Filings API (https://beatandraise.com/sec-filings-api).
2
+
3
+ Standard library only. Every method maps to one endpoint of https://beatandraise.com/api/v1/openapi.json
4
+ and returns the decoded JSON (a dict), except the methods that return a document's text or a file's bytes.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ import time
12
+ import urllib.error
13
+ import urllib.parse
14
+ import urllib.request
15
+ from typing import Any, Dict, Optional, Union
16
+
17
+ from . import __version__
18
+
19
+ DEFAULT_BASE_URL = "https://beatandraise.com/api/v1"
20
+
21
+
22
+ class APIError(Exception):
23
+ """A request the API refused or failed. `status` is the HTTP status, `body` the decoded error body."""
24
+
25
+ def __init__(self, status: int, body: Any, url: str):
26
+ self.status = status
27
+ self.body = body
28
+ self.url = url
29
+ if isinstance(body, dict):
30
+ msg = body.get("message") or body.get("error") or json.dumps(body)
31
+ else:
32
+ msg = str(body)[:500]
33
+ super().__init__(f"HTTP {status}: {msg}")
34
+
35
+
36
+ def _clean(params: Dict[str, Any]) -> Dict[str, str]:
37
+ out = {}
38
+ for k, v in params.items():
39
+ if v is None:
40
+ continue
41
+ k = k.rstrip("_") # from_ -> from
42
+ if isinstance(v, bool):
43
+ v = "true" if v else "false"
44
+ elif isinstance(v, (list, tuple)):
45
+ v = ",".join(str(x) for x in v)
46
+ out[k] = str(v)
47
+ return out
48
+
49
+
50
+ def _seg(s: Any) -> str:
51
+ return urllib.parse.quote(str(s), safe="")
52
+
53
+
54
+ class Client:
55
+ """SEC EDGAR filings and the structured data built on them.
56
+
57
+ >>> from beatandraise import Client
58
+ >>> api = Client() # reads BEATANDRAISE_API_KEY
59
+ >>> api.filings(cik="0000320193", form="10-K", limit=1)
60
+
61
+ Get a free key at https://beatandraise.com/api-keys.
62
+ """
63
+
64
+ def __init__(
65
+ self,
66
+ api_key: Optional[str] = None,
67
+ base_url: str = DEFAULT_BASE_URL,
68
+ timeout: float = 60.0,
69
+ max_retries: int = 2,
70
+ ):
71
+ self.api_key = api_key or os.environ.get("BEATANDRAISE_API_KEY") or os.environ.get("BAR_KEY")
72
+ if not self.api_key:
73
+ raise ValueError(
74
+ "No API key: pass api_key= or set BEATANDRAISE_API_KEY. "
75
+ "Create a free key at https://beatandraise.com/api-keys"
76
+ )
77
+ self.base_url = base_url.rstrip("/")
78
+ self.timeout = timeout
79
+ self.max_retries = max_retries
80
+
81
+ # -- transport ---------------------------------------------------------------------------------------
82
+
83
+ def _request(self, method: str, path: str, params: Optional[Dict[str, Any]] = None,
84
+ body: Optional[Dict[str, Any]] = None, raw: bool = False):
85
+ url = self.base_url + path
86
+ q = _clean(params or {})
87
+ if q:
88
+ url += "?" + urllib.parse.urlencode(q)
89
+ data = json.dumps(body).encode() if body is not None else None
90
+ headers = {
91
+ "X-API-Key": self.api_key,
92
+ "User-Agent": f"beatandraise-python/{__version__}",
93
+ "Accept": "application/json, text/markdown, */*",
94
+ }
95
+ if data is not None:
96
+ headers["Content-Type"] = "application/json"
97
+ attempt = 0
98
+ while True:
99
+ req = urllib.request.Request(url, data=data, method=method, headers=headers)
100
+ try:
101
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
102
+ payload = resp.read()
103
+ if raw:
104
+ return payload, dict(resp.headers)
105
+ ctype = resp.headers.get("Content-Type", "")
106
+ if "json" in ctype:
107
+ return json.loads(payload)
108
+ return payload.decode("utf-8", "replace")
109
+ except urllib.error.HTTPError as e:
110
+ payload = e.read()
111
+ try:
112
+ err = json.loads(payload)
113
+ except ValueError:
114
+ err = payload.decode("utf-8", "replace")
115
+ if e.code in (429, 502, 503, 504) and attempt < self.max_retries:
116
+ wait = e.headers.get("Retry-After")
117
+ try:
118
+ delay = min(float(wait), 30.0) if wait else 1.0 * (2 ** attempt)
119
+ except ValueError:
120
+ delay = 1.0 * (2 ** attempt)
121
+ time.sleep(delay)
122
+ attempt += 1
123
+ continue
124
+ raise APIError(e.code, err, url) from None
125
+
126
+ def get(self, path: str, **params):
127
+ """Any GET endpoint, e.g. api.get("/company/0000320193")."""
128
+ return self._request("GET", path, params)
129
+
130
+ # -- find ------------------------------------------------------------------------------------------
131
+
132
+ def filings(self, cik: Optional[str] = None, form: Optional[str] = None, from_: Optional[str] = None,
133
+ to: Optional[str] = None, item=None, item_text=None, include_item_text: Optional[bool] = None,
134
+ item_text_chars: Optional[int] = None, limit: Optional[int] = None, offset: Optional[int] = None):
135
+ """List filings by company, form type and date; for 8-Ks, by Item (item="5.02")."""
136
+ return self._request("GET", "/filings", dict(
137
+ cik=cik, form=form, from_=from_, to=to, item=item, item_text=item_text,
138
+ include_item_text=include_item_text, item_text_chars=item_text_chars, limit=limit, offset=offset))
139
+
140
+ def search(self, q: str, form: Optional[str] = None, cik: Optional[str] = None, year: Optional[int] = None,
141
+ limit: Optional[int] = None, page: Optional[int] = None, by_company: Optional[bool] = None):
142
+ """Full-text search across filings (2020 on). by_company=True gives each company's best match."""
143
+ return self._request("GET", "/search", dict(q=q, form=form, cik=cik, year=year, limit=limit,
144
+ page=page, by_company=by_company))
145
+
146
+ def search_companies(self, q: str, limit: Optional[int] = None):
147
+ """Find a company's CIK by name or ticker."""
148
+ return self._request("GET", "/company/search", dict(q=q, limit=limit))
149
+
150
+ def company(self, cik: str):
151
+ return self._request("GET", f"/company/{_seg(cik)}")
152
+
153
+ def identifiers(self, ticker: Optional[str] = None, cik: Optional[str] = None, cusip: Optional[str] = None,
154
+ limit: Optional[int] = None):
155
+ """Ticker, CIK and CUSIP: give one, get the others."""
156
+ return self._request("GET", "/identifiers", dict(ticker=ticker, cik=cik, cusip=cusip, limit=limit))
157
+
158
+ def sic(self, **params):
159
+ """SIC industry codes and the companies in them (find=, code=, exchange=, ...)."""
160
+ return self._request("GET", "/sic", params)
161
+
162
+ def fiscal_calendar(self, cik: str, fiscal_year: Optional[int] = None, from_: Optional[str] = None,
163
+ to: Optional[str] = None, limit: Optional[int] = None):
164
+ """Every fiscal quarter a company reported, with exact start and end dates."""
165
+ return self._request("GET", "/fiscal-calendar", dict(cik=cik, fiscal_year=fiscal_year, from_=from_,
166
+ to=to, limit=limit))
167
+
168
+ # -- read ------------------------------------------------------------------------------------------
169
+
170
+ def filing(self, accession: str):
171
+ """One filing's metadata and its files."""
172
+ return self._request("GET", f"/filing/{_seg(accession)}")
173
+
174
+ def filing_tables(self, accession: str):
175
+ """The XBRL viewer pages (R-pages) of a filing."""
176
+ return self._request("GET", f"/filing/{_seg(accession)}/tables")
177
+
178
+ def files(self, accession: str):
179
+ return self._request("GET", f"/files/{_seg(accession)}")
180
+
181
+ def download(self, accession: str, filename: str) -> bytes:
182
+ """A stored file's bytes, as filed (counts against the plan's download allowance)."""
183
+ payload, _ = self._request("GET", f"/files/{_seg(accession)}/{_seg(filename)}", raw=True)
184
+ return payload
185
+
186
+ def document(self, cik: str, accession: str, filename: str, markdown: bool = True) -> str:
187
+ """A document's text (markdown) or, with markdown=False, its HTML/XML."""
188
+ return self._request("GET", f"/document/{_seg(cik)}/{_seg(accession)}/{_seg(filename)}",
189
+ dict(get_markdown=markdown))
190
+
191
+ def search_document(self, cik: str, accession: str, q: str, filename: Optional[str] = None,
192
+ context: Optional[int] = None):
193
+ """Lines matching terms within one filing (every readable document, or one filename)."""
194
+ path = f"/document/search/{_seg(cik)}/{_seg(accession)}"
195
+ if filename:
196
+ path += f"/{_seg(filename)}"
197
+ return self._request("GET", path, dict(q=q, context=context))
198
+
199
+ def items(self, accession: str):
200
+ """The Items of a 10-K, 10-Q, 8-K or 20-F, with where each begins."""
201
+ return self._request("GET", f"/items/{_seg(accession)}")
202
+
203
+ def item(self, accession: str, item: str, part: Optional[str] = None, format: Optional[str] = None) -> str:
204
+ """One Item's text, e.g. item(acc, "1A") for Risk Factors or "7" for MD&A."""
205
+ return self._request("GET", f"/items/{_seg(accession)}/{_seg(item)}", dict(part=part, format=format))
206
+
207
+ # -- structured ------------------------------------------------------------------------------------
208
+
209
+ def form_data(self, cik: str, form: str, **params):
210
+ """Every filing of one XML form (Form 4, 144, 13F, D...) for a CIK as JSON or XML."""
211
+ return self._request("GET", "/form-data", dict(cik=cik, form=form, **params))
212
+
213
+ def statements(self, cik: str, accession: str, include_tags: Optional[bool] = None):
214
+ """Income statement, balance sheet and cash flow of one 10-K/10-Q/20-F/40-F, as presented."""
215
+ return self._request("GET", f"/statements/{_seg(cik)}/{_seg(accession)}",
216
+ dict(include_tags=include_tags))
217
+
218
+ def financial_history(self, cik: str, period: Optional[str] = None, statements=None,
219
+ limit: Optional[int] = None, before: Optional[str] = None,
220
+ fiscal_year: Optional[int] = None, fiscal_period: Optional[str] = None,
221
+ include_tags: Optional[bool] = None):
222
+ """The same statements across a company's last several annual (or quarterly) reports."""
223
+ return self._request("GET", "/financial-history", dict(
224
+ cik=cik, period=period, statements=statements, limit=limit, before=before,
225
+ fiscal_year=fiscal_year, fiscal_period=fiscal_period, include_tags=include_tags))
226
+
227
+ def insider_transactions(self, cik: str, from_: Optional[str] = None, to: Optional[str] = None,
228
+ limit: Optional[int] = None, offset: Optional[int] = None,
229
+ include_table_ii: Optional[bool] = None, include_footnotes: Optional[bool] = None):
230
+ """Form 4 transactions, newest first, as filed."""
231
+ return self._request("GET", "/insider-transactions", dict(
232
+ cik=cik, from_=from_, to=to, limit=limit, offset=offset,
233
+ include_table_ii=include_table_ii, include_footnotes=include_footnotes))
234
+
235
+ def shares_outstanding(self, cik: str, share_class: Optional[str] = None, from_: Optional[str] = None,
236
+ to: Optional[str] = None):
237
+ """Cover-page share counts per class over time, with stock splits."""
238
+ return self._request("GET", "/shares-outstanding", dict(cik=cik, share_class=share_class,
239
+ from_=from_, to=to))
240
+
241
+ def holdings(self, cik: str, period: Optional[str] = None, limit: Optional[int] = None,
242
+ offset: Optional[int] = None):
243
+ """One 13F filer's positions for a report period."""
244
+ return self._request("GET", f"/holdings/{_seg(cik)}", dict(period=period, limit=limit, offset=offset))
245
+
246
+ def institutional_holders(self, company: str, quarters: Optional[int] = None, holder: Optional[str] = None,
247
+ limit: Optional[int] = None, detail: Optional[str] = None):
248
+ """Who holds a company (ticker or CIK) by 13F, quarter by quarter."""
249
+ return self._request("GET", f"/institutional-holders/{_seg(company)}", dict(
250
+ quarters=quarters, holder=holder, limit=limit, detail=detail))
251
+
252
+ # -- verify ----------------------------------------------------------------------------------------
253
+
254
+ def verify_quote(self, cik: str, accession: str, filename: str, quote: str):
255
+ """Is this exact text in the document? Returns highlight links to each occurrence."""
256
+ return self._request("GET", f"/document/verify/{_seg(cik)}/{_seg(accession)}/{_seg(filename)}",
257
+ dict(quote=quote))
258
+
259
+ def verify_figure(self, cik: str, accession: str, value: str, label: Optional[str] = None,
260
+ filename: Optional[str] = None):
261
+ """Is this figure in the filing, however it is written ("$416.2 billion" vs "416,161")?"""
262
+ path = f"/document/verify-figure/{_seg(cik)}/{_seg(accession)}"
263
+ if filename:
264
+ path += f"/{_seg(filename)}"
265
+ return self._request("GET", path, dict(value=value, label=label))
266
+
267
+ def figures(self, cik: str, accession: str, filename: str, near: Optional[str] = None,
268
+ limit: Optional[int] = None):
269
+ """A document's numbers with their labels and refs, for citing."""
270
+ return self._request("GET", f"/document/figures/{_seg(cik)}/{_seg(accession)}/{_seg(filename)}",
271
+ dict(near=near, limit=limit))
272
+
273
+ def check_citations(self, text: str):
274
+ """Check every [ref:...] and [calc: ...] citation in a draft against the filings."""
275
+ return self._request("POST", "/check-citations", body={"text": text})
276
+
277
+ # -- account ---------------------------------------------------------------------------------------
278
+
279
+ def account(self):
280
+ """Your plan, limits and usage."""
281
+ return self._request("GET", "/account")
@@ -0,0 +1,57 @@
1
+ """Live smoke test: runs the README's calls against the API.
2
+
3
+ BEATANDRAISE_API_KEY=bar_... python -m pytest tests/ (or python tests/test_live.py)
4
+ BEATANDRAISE_BASE_URL overrides the base URL. Skipped without a key.
5
+ """
6
+ import os
7
+ import unittest
8
+
9
+ from beatandraise import Client, APIError, DEFAULT_BASE_URL
10
+
11
+ KEY = os.environ.get("BEATANDRAISE_API_KEY")
12
+ BASE = os.environ.get("BEATANDRAISE_BASE_URL", DEFAULT_BASE_URL)
13
+ APPLE = "0000320193"
14
+
15
+
16
+ @unittest.skipUnless(KEY, "set BEATANDRAISE_API_KEY")
17
+ class Live(unittest.TestCase):
18
+ @classmethod
19
+ def setUpClass(cls):
20
+ cls.api = Client(KEY, base_url=BASE)
21
+ cls.acc = cls.api.filings(cik=APPLE, form="10-K", limit=1)["filings"][0]["accession_number"]
22
+
23
+ def test_find(self):
24
+ self.assertTrue(self.api.search_companies("AAPL"))
25
+ self.assertIn("quarters", self.api.fiscal_calendar(APPLE, limit=4))
26
+ hits = self.api.search('"going concern"', form="10-K", year=2025, by_company=True, limit=3)
27
+ self.assertIn("companies_found", hits)
28
+
29
+ def test_read(self):
30
+ items = self.api.items(self.acc)["items"]
31
+ self.assertTrue(any(i["item"] == "1A" for i in items))
32
+ text = self.api.item(self.acc, "1A")
33
+ self.assertIn("Risk Factors", text[:200])
34
+ self.assertTrue(self.api.search_document(APPLE, self.acc, q="tariffs"))
35
+
36
+ def test_structured(self):
37
+ h = self.api.financial_history(APPLE, period="annual", statements="income", limit=2)
38
+ self.assertTrue(h["filings"])
39
+ s = self.api.statements(APPLE, self.acc, include_tags=True)
40
+ self.assertTrue(any(st.get("lines") for st in s["statements"]))
41
+ self.assertIn("transactions", self.api.insider_transactions("0001045810", from_="2026-09-01",
42
+ include_footnotes=True))
43
+ self.assertTrue(self.api.institutional_holders("AAPL")["quarters"])
44
+ self.assertIn("filings", self.api.filings(item="5.02", item_text="5.02", from_="2026-11-01", limit=2))
45
+
46
+ def test_verify(self):
47
+ v = self.api.verify_figure(APPLE, self.acc, value="$478.8 billion", label="Net sales")
48
+ self.assertTrue(v.get("found"))
49
+
50
+ def test_error(self):
51
+ with self.assertRaises(APIError) as cm:
52
+ self.api.filing("0000000000-00-000000")
53
+ self.assertEqual(cm.exception.status, 404)
54
+
55
+
56
+ if __name__ == "__main__":
57
+ unittest.main()