Metadata-Version: 2.5
Name: thaler
Version: 0.1.0
Summary: The Thaler API from Python: financial data from SEC filings, with the source filing for every value.
Project-URL: Homepage, https://thaler.sh/developers
Project-URL: Documentation, https://thaler.sh/developers
Project-URL: Changelog, https://thaler.sh/developers/changelog
Author-email: Thaler <support@thaler.sh>
License-Expression: MIT
License-File: LICENSE
Keywords: 13f,edgar,financial-data,fundamentals,insider-trading,sec,thaler,xbrl
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Office/Business :: Financial :: Investment
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx<1,>=0.27
Provides-Extra: dev
Requires-Dist: mypy>=1.14; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff<0.16,>=0.15; extra == 'dev'
Description-Content-Type: text/markdown

# thaler

The [Thaler API](https://thaler.sh/developers) from Python: financial data
from SEC filings, with the source filing for every value.

```sh
pip install thaler
```

```python
import thaler

client = thaler.Thaler()  # reads THALER_API_KEY; or Thaler(api_key="thaler_…")

rows = client.metrics("AAPL", period="annual", keys=["revenue", "net_income"])
for row in rows.data:
    source = row.read_from.accession_number if row.read_from else None
    print(row.end_date, row.metric_key, row.value, source)
# 2025-09-27 revenue 416161000000 0000320193-25-000079
```

Every figure is a `decimal.Decimal`, every day a `datetime.date`, and
every answer a `Response` with the request ID and the account’s limits
beside its `data`. Create a key at
[thaler.sh/developers/keys](https://thaler.sh/developers/keys).

## What the client does

At most four requests are in flight at once. When the minute’s sixty are
spent, the next request waits for the reset. A 429 is retried after its
`Retry-After`; one longer than a minute, the month’s limit, raises
`RateLimitError` at once.

A 500, 502, 503 or 504, or a connection that failed, is retried twice
after a growing pause; those answers don’t count against the month. A
400, 401 or 404 is raised as it stands, and a Screener clause the API
would refuse is a `ValueError` before anything is sent.

Figures are `Decimal` (a float loses digits past the fifteenth), days
are `date`, instants are `datetime`, and each model is a frozen
dataclass with `None` for what is absent or null. An answer that is not
as documented raises `DecodeError`, naming the field and the request ID.

`iter_screen`, `iter_holders`, `iter_holder_positions` and
`iter_insider_filings` yield rows across pages.

`NotFoundError`, `AuthenticationError`, `BadRequestError`,
`RateLimitError` and `ServerError` are each an `APIError` with the
problem’s `code`, `detail` and `request_id`.

## The client

```python
client = thaler.Thaler(
    api_key=None,          # or THALER_API_KEY
    timeout=30.0,          # seconds to wait for an answer
    max_retries=2,         # tries after the first, on 429, 500, 502, 503, 504 or a lost connection
    max_concurrent=4,      # requests in flight; the API allows four
    max_retry_after=60.0,  # the longest Retry-After waited for
)
```

`with thaler.Thaler() as client:` closes the connections on the way out.
`thaler.AsyncThaler` is the same client for asyncio: every method
awaited, every `iter_*` an async iterator.

## Every call

| Call | Answers with |
| --- | --- |
| `search_securities(query)` | `list[SecuritySearchHit]` |
| `profile(ticker)` | `SecurityProfile` |
| `metrics(ticker, period=, keys=, collapse=, limit=, as_of=)` | `list[MetricValue]` |
| `metric_catalog()` | `list[MetricCatalogEntry]` |
| `metric_lineage(ticker, metric_key, metric_value_id=, limit=, as_of=)` | `list[MetricLineage]` |
| `metric_revisions(ticker, metric_key, metric_value_id=, fiscal_year=, fiscal_period=, limit=)` | `list[MetricRevision]` |
| `raw_concepts(ticker, limit=)` | `list[RawConcept]` |
| `filings(ticker, forms=, items=, limit=)` | `list[Filing]` |
| `filings_day(date=)` | `FilingsDay` |
| `filing(accession)` | `FilingSource` |
| `insider_activity(ticker=None, tickers=, since=, kind=, limit=, offset=)` | `InsiderActivityPage` |
| `search_holders(query=, limit=)` | `list[HolderHit]` |
| `holder(cik, limit=, offset=)` | `Holder` |
| `holders(ticker, limit=, offset=)` | `SecurityHolders` |
| `segments(ticker, period=)` | `Segments` |
| `prices(ticker, range=, from_=, to=)` | `Prices` |
| `screen(where=, sort=, dir=, limit=, offset=, columns=)` | `list[ScreenRow]` |
| `release()` | `Release` |

Each returns a `Response`: `.data` as above, `.meta` (the route, the
parameters as the API read them, the Screener’s counts, the release),
`.request_id`, `.etag`, `.rate_limit` and `.headers`.

## The Screener

```python
from thaler import where

big_and_profitable = client.screen(
    where=[where("revenue", ">=", 10_000_000_000), where("net_margin", ">", 0.2)],
    sort="revenue",
    dir="desc",
    columns=["revenue", "net_margin", "market_cap"],
)
for row in big_and_profitable.data:
    print(row.ticker, row.revenue, row.net_margin)
print(big_and_profitable.meta.total, "matches")

for row in client.iter_screen(where=[where("fcf_margin", ">=", 0.15)], limit=1000):
    ...
```

`where` writes a clause as the API takes it (`revenue:gte:10000000000`,
never scientific notation); a clause written by hand works as well.

## Point-in-time reads

```python
then = client.metrics("KHC", period="annual", keys=["net_income"], as_of="2019-03-01")
now = client.metrics("KHC", period="annual", keys=["net_income"])
```

Each value comes from the latest filing on or before the day, so a
backtest only sees what was public at the time. See the guide on
[point-in-time data](https://thaler.sh/developers/guides/as-of).

## Errors

```python
try:
    client.profile("ZZZZ")
except thaler.NotFoundError as error:
    print(error.code, error.detail, error.request_id)
except thaler.RateLimitError as error:
    print("wait", error.retry_after, "seconds;", error.violated_policies)
except thaler.APIError as error:
    print(error.status, error.code)
except thaler.TransportError:
    print("no answer")
```

## Prices

Prices include IEX’s last sale for each trading day. Data provided for
free by IEX. By accessing or using IEX Historical Data, you agree to the
[IEX Historical Data Terms of Use](https://www.iex.io/legal/hist-data-terms).

## Versions

The SDK is `0.x` while the API is in beta. `thaler.API_VERSION` names
the API document a release follows. Changes are in `CHANGELOG.md`,
shipped with the package.
