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.
- beatandraise-0.1.0/.gitignore +6 -0
- beatandraise-0.1.0/LICENSE +21 -0
- beatandraise-0.1.0/PKG-INFO +138 -0
- beatandraise-0.1.0/README.md +116 -0
- beatandraise-0.1.0/pyproject.toml +33 -0
- beatandraise-0.1.0/src/beatandraise/__init__.py +7 -0
- beatandraise-0.1.0/src/beatandraise/client.py +281 -0
- beatandraise-0.1.0/tests/test_live.py +57 -0
|
@@ -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,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()
|