filingstudio 0.4.0__tar.gz → 0.5.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.
- {filingstudio-0.4.0 → filingstudio-0.5.0}/PKG-INFO +4 -1
- {filingstudio-0.4.0 → filingstudio-0.5.0}/README.md +3 -0
- {filingstudio-0.4.0 → filingstudio-0.5.0}/filingstudio/__init__.py +1 -1
- {filingstudio-0.4.0 → filingstudio-0.5.0}/filingstudio/client.py +55 -1
- {filingstudio-0.4.0 → filingstudio-0.5.0}/pyproject.toml +1 -1
- {filingstudio-0.4.0 → filingstudio-0.5.0}/tests/test_client.py +44 -0
- {filingstudio-0.4.0 → filingstudio-0.5.0}/.gitignore +0 -0
- {filingstudio-0.4.0 → filingstudio-0.5.0}/LICENSE +0 -0
- {filingstudio-0.4.0 → filingstudio-0.5.0}/filingstudio/errors.py +0 -0
- {filingstudio-0.4.0 → filingstudio-0.5.0}/filingstudio/models.py +0 -0
- {filingstudio-0.4.0 → filingstudio-0.5.0}/filingstudio/research.py +0 -0
- {filingstudio-0.4.0 → filingstudio-0.5.0}/tests/test_research.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: filingstudio
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Client for the Filing Studio API: search SEC filings, resolve traces, verify claims. One call per door, typed results, your key never in a URL.
|
|
5
5
|
Project-URL: Homepage, https://filingstudio.com
|
|
6
6
|
Project-URL: Documentation, https://filingstudio.com/docs#sdk
|
|
@@ -81,6 +81,9 @@ async with AsyncFilingStudio(api_key=key) as fs:
|
|
|
81
81
|
| `filings(ticker, form=, year=, limit=)` | a company's indexed filings |
|
|
82
82
|
| `coverage(ticker)` | is anything indexed, and how fresh |
|
|
83
83
|
| `table(ticker, accession, table_id, format="records")` | one printed table, as filed |
|
|
84
|
+
| `kpi_tables(ticker, quarterly=False)` | the newest 10-K's (10-Q's) non-statement tables, as a dict |
|
|
85
|
+
| `kpi_series(ticker, table_id, quarterly=False, years=10)` | one table across filings as rows x periods, as a dict |
|
|
86
|
+
| `statement(ticker, kind, quarterly=False, years=10)` | `income`, `balance` or `cashflow` as a time series, as a dict |
|
|
84
87
|
|
|
85
88
|
Pass `value` to `verify` as the raw figure you hold (130497 or 130497000000
|
|
86
89
|
alike). The API tries every printed scale a filer could use. Do not pre-scale.
|
|
@@ -48,6 +48,9 @@ async with AsyncFilingStudio(api_key=key) as fs:
|
|
|
48
48
|
| `filings(ticker, form=, year=, limit=)` | a company's indexed filings |
|
|
49
49
|
| `coverage(ticker)` | is anything indexed, and how fresh |
|
|
50
50
|
| `table(ticker, accession, table_id, format="records")` | one printed table, as filed |
|
|
51
|
+
| `kpi_tables(ticker, quarterly=False)` | the newest 10-K's (10-Q's) non-statement tables, as a dict |
|
|
52
|
+
| `kpi_series(ticker, table_id, quarterly=False, years=10)` | one table across filings as rows x periods, as a dict |
|
|
53
|
+
| `statement(ticker, kind, quarterly=False, years=10)` | `income`, `balance` or `cashflow` as a time series, as a dict |
|
|
51
54
|
|
|
52
55
|
Pass `value` to `verify` as the raw figure you hold (130497 or 130497000000
|
|
53
56
|
alike). The API tries every printed scale a filer could use. Do not pre-scale.
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""
|
|
2
2
|
The Filing Studio client, sync and async. One method per /v1 door, the same
|
|
3
|
-
names as the Node `@filingstudio/client` and the browser `ProvenanceClient
|
|
3
|
+
names as the Node `@filingstudio/client` and the browser `ProvenanceClient`,
|
|
4
|
+
plus three research doors (kpi_tables, kpi_series, statement) that answer
|
|
5
|
+
the research JSON as a plain dict.
|
|
4
6
|
|
|
5
7
|
from filingstudio import FilingStudio
|
|
6
8
|
fs = FilingStudio(api_key=os.environ["FILING_STUDIO_API_KEY"])
|
|
@@ -36,6 +38,7 @@ from .models import (
|
|
|
36
38
|
)
|
|
37
39
|
|
|
38
40
|
DEFAULT_BASE = "https://api.filingstudio.com"
|
|
41
|
+
STATEMENT_KINDS = ("income", "balance", "cashflow")
|
|
39
42
|
|
|
40
43
|
|
|
41
44
|
def _clean(params: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -50,6 +53,9 @@ def _describe(status: int, body: Any) -> "tuple[str, Optional[str]]":
|
|
|
50
53
|
msg = err.get("message") if isinstance(err.get("message"), str) else f"HTTP {status}"
|
|
51
54
|
code = err.get("code") if isinstance(err.get("code"), str) else None
|
|
52
55
|
return msg, code
|
|
56
|
+
detail = body.get("detail") if isinstance(body, dict) else None
|
|
57
|
+
if isinstance(detail, str):
|
|
58
|
+
return detail, None
|
|
53
59
|
return f"HTTP {status}", None
|
|
54
60
|
|
|
55
61
|
|
|
@@ -179,6 +185,22 @@ class _Base:
|
|
|
179
185
|
return (f"/v1/tables/{quote(ticker.upper(), safe='')}/{quote(accession, safe='')}/"
|
|
180
186
|
f"{quote(table_id, safe='')}")
|
|
181
187
|
|
|
188
|
+
# The research doors answer the raw research JSON, not the /v1 envelope.
|
|
189
|
+
|
|
190
|
+
@staticmethod
|
|
191
|
+
def _kpi_tables_path(ticker: str) -> str:
|
|
192
|
+
return f"/api/research/kpi/tables/{quote(ticker.upper(), safe='')}"
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def _kpi_series_path(ticker: str) -> str:
|
|
196
|
+
return f"/api/research/kpi/series/{quote(ticker.upper(), safe='')}"
|
|
197
|
+
|
|
198
|
+
@staticmethod
|
|
199
|
+
def _statement_path(ticker: str, kind: str) -> str:
|
|
200
|
+
if kind not in STATEMENT_KINDS:
|
|
201
|
+
raise ValueError(f"kind is one of {', '.join(STATEMENT_KINDS)}")
|
|
202
|
+
return f"/api/research/statements/{quote(ticker.upper(), safe='')}/{kind}"
|
|
203
|
+
|
|
182
204
|
|
|
183
205
|
class FilingStudio(_Base):
|
|
184
206
|
"""Synchronous client. Use as a context manager to reuse one connection."""
|
|
@@ -258,6 +280,25 @@ class FilingStudio(_Base):
|
|
|
258
280
|
return self._table(self._request("GET", self._table_path(ticker, accession, table_id),
|
|
259
281
|
params={"format": format}))
|
|
260
282
|
|
|
283
|
+
def kpi_tables(self, ticker: str, *, quarterly: bool = False) -> Dict[str, Any]:
|
|
284
|
+
"""The newest 10-K's (10-Q's) non-statement tables, named by the text above them:
|
|
285
|
+
{ticker, accession, form, filed, tables}."""
|
|
286
|
+
return self._request("GET", self._kpi_tables_path(ticker), params={"quarterly": quarterly})
|
|
287
|
+
|
|
288
|
+
def kpi_series(self, ticker: str, table_id: str, *, quarterly: bool = False,
|
|
289
|
+
years: int = 10) -> Dict[str, Any]:
|
|
290
|
+
"""One printed table carried across filings as rows x periods, every value traced:
|
|
291
|
+
{ticker, tableId, above, units, periods, sources, rows, gaps}."""
|
|
292
|
+
return self._request("GET", self._kpi_series_path(ticker),
|
|
293
|
+
params={"table_id": table_id, "quarterly": quarterly, "years": years})
|
|
294
|
+
|
|
295
|
+
def statement(self, ticker: str, kind: str, *, quarterly: bool = False,
|
|
296
|
+
years: int = 10) -> Dict[str, Any]:
|
|
297
|
+
"""A primary statement (income, balance, cashflow) as a time series from each
|
|
298
|
+
filing's printed table. No model."""
|
|
299
|
+
return self._request("GET", self._statement_path(ticker, kind),
|
|
300
|
+
params={"quarterly": quarterly, "years": years})
|
|
301
|
+
|
|
261
302
|
|
|
262
303
|
class AsyncFilingStudio(_Base):
|
|
263
304
|
"""Asynchronous client with the same methods."""
|
|
@@ -331,3 +372,16 @@ class AsyncFilingStudio(_Base):
|
|
|
331
372
|
format: str = "records") -> TableResult:
|
|
332
373
|
return self._table(await self._request("GET", self._table_path(ticker, accession, table_id),
|
|
333
374
|
params={"format": format}))
|
|
375
|
+
|
|
376
|
+
async def kpi_tables(self, ticker: str, *, quarterly: bool = False) -> Dict[str, Any]:
|
|
377
|
+
return await self._request("GET", self._kpi_tables_path(ticker), params={"quarterly": quarterly})
|
|
378
|
+
|
|
379
|
+
async def kpi_series(self, ticker: str, table_id: str, *, quarterly: bool = False,
|
|
380
|
+
years: int = 10) -> Dict[str, Any]:
|
|
381
|
+
return await self._request("GET", self._kpi_series_path(ticker),
|
|
382
|
+
params={"table_id": table_id, "quarterly": quarterly, "years": years})
|
|
383
|
+
|
|
384
|
+
async def statement(self, ticker: str, kind: str, *, quarterly: bool = False,
|
|
385
|
+
years: int = 10) -> Dict[str, Any]:
|
|
386
|
+
return await self._request("GET", self._statement_path(ticker, kind),
|
|
387
|
+
params={"quarterly": quarterly, "years": years})
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "filingstudio"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Client for the Filing Studio API: search SEC filings, resolve traces, verify claims. One call per door, typed results, your key never in a URL."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
@@ -207,3 +207,47 @@ async def test_async_client_mirrors_sync():
|
|
|
207
207
|
v = await fs.verify("NVDA", metric="Revenue", value=1)
|
|
208
208
|
assert v.verdict == "unsupported" and v.actual[0].printed_text == "130,497"
|
|
209
209
|
assert seen[-1].headers["x-api-key"] == KEY
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_research_doors_paths_params_and_raw_json():
|
|
213
|
+
fs, seen = make(lambda r, n: ok({"ticker": "LEN", "tables": [{"tableId": "t1"}]}))
|
|
214
|
+
assert fs.kpi_tables("len", quarterly=True)["tables"][0]["tableId"] == "t1"
|
|
215
|
+
fs.kpi_series("len", "primary:83", years=5)
|
|
216
|
+
fs.statement("len", "cashflow", quarterly=True, years=3)
|
|
217
|
+
assert [r.url.path for r in seen] == [
|
|
218
|
+
"/api/research/kpi/tables/LEN",
|
|
219
|
+
"/api/research/kpi/series/LEN",
|
|
220
|
+
"/api/research/statements/LEN/cashflow",
|
|
221
|
+
]
|
|
222
|
+
assert dict(seen[0].url.params) == {"quarterly": "true"}
|
|
223
|
+
assert dict(seen[1].url.params) == {"table_id": "primary:83", "quarterly": "false", "years": "5"}
|
|
224
|
+
assert dict(seen[2].url.params) == {"quarterly": "true", "years": "3"}
|
|
225
|
+
assert all(r.headers["x-api-key"] == KEY and KEY not in str(r.url) for r in seen)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_statement_rejects_unknown_kind_before_calling():
|
|
229
|
+
fs, seen = make(lambda r, n: ok({}))
|
|
230
|
+
with pytest.raises(ValueError):
|
|
231
|
+
fs.statement("LEN", "equity")
|
|
232
|
+
assert seen == []
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_research_door_404_detail_becomes_the_message():
|
|
236
|
+
fs, _ = make(lambda r, n: httpx.Response(404, json={"detail": "No 10-K for ZZZZ"}))
|
|
237
|
+
with pytest.raises(FilingStudioError) as e:
|
|
238
|
+
fs.kpi_tables("ZZZZ")
|
|
239
|
+
assert e.value.status == 404
|
|
240
|
+
assert "No 10-K for ZZZZ" in str(e.value)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@pytest.mark.anyio
|
|
244
|
+
async def test_async_research_doors():
|
|
245
|
+
fs, seen = amake(lambda r, n: ok({"rows": []}))
|
|
246
|
+
assert await fs.statement("nvda", "income") == {"rows": []}
|
|
247
|
+
await fs.kpi_series("nvda", "t9")
|
|
248
|
+
await fs.kpi_tables("nvda")
|
|
249
|
+
assert [r.url.path for r in seen] == [
|
|
250
|
+
"/api/research/statements/NVDA/income",
|
|
251
|
+
"/api/research/kpi/series/NVDA",
|
|
252
|
+
"/api/research/kpi/tables/NVDA",
|
|
253
|
+
]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|