Metadata-Version: 2.4
Name: palanor
Version: 0.1.0
Summary: Python client for Palanor Data: signals, indices and refined datasets, with provenance on every result.
Project-URL: Homepage, https://palanor.com/data
Project-URL: Documentation, https://palanor.com/developers#python
Project-URL: API reference, https://api.palanor.com/openapi.json
Author-email: "Palanor, Inc." <developers@palanor.com>
License-Expression: MIT
License-File: LICENSE
Keywords: api client,economic data,macro,palanor,sec filings,time series
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Intended Audience :: Science/Research
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Office/Business :: Financial
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx<1,>=0.24
Provides-Extra: dev
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pandas-stubs; extra == 'dev'
Requires-Dist: pandas>=1.5; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Provides-Extra: pandas
Requires-Dist: pandas>=1.5; extra == 'pandas'
Description-Content-Type: text/markdown

# palanor

The Python client for [Palanor Data](https://palanor.com/data): the signals, Custom Indices and refined
datasets of the Palanor lattice, with the provenance of every value kept on the result.

- One typed client over the REST API at `https://api.palanor.com/v1`
- Pages are followed for you; `max_rows` caps a long read
- Retries with exponential backoff on transient errors (429, 502, 503, 504, database timeouts)
- `to_pandas()` on every result, with provenance kept in `df.attrs`
- Python 3.9+, one dependency (`httpx`); pandas is optional

## Install

```bash
pip install "https://palanor.com/developers/python/palanor-0.1.0-py3-none-any.whl"
# with DataFrames:
pip install "palanor[pandas] @ https://palanor.com/developers/python/palanor-0.1.0-py3-none-any.whl"
```

Mint a free key at [palanor.com/api-tokens](https://palanor.com/api-tokens) (1,000 requests a month, no card), then:

```bash
export PALANOR_API_KEY="plnr_..."
```

## Quickstart

```python
from palanor import Palanor

p = Palanor()  # reads PALANOR_API_KEY; or Palanor(api_key="plnr_...")

# A series over a window: every page joined, oldest first
spread = p.signals.observations("rates.spread_2y10y", start="2020-01-01", end="2024-12-31")
print(len(spread), spread.observations[-1])
print(spread.provenance.attribution)      # the credit line owed when you show it

df = spread.to_pandas()                   # DatetimeIndex, one "value" column
df.attrs["provenance"]["license_class"]   # 'public_domain'

# The latest reading, and where it stands in its own record
cpi = p.signals.get("bls_cpi_college_tuition", history=12)
cpi.latest.value, cpi.latest.date, cpi.context["percentile"]
```

## What you can read

| Method | Endpoint | Notes |
|---|---|---|
| `p.signals.list(category=None)` | `/v1/signals` | Every public signal the API serves, each row with `provenance` |
| `p.signals.get(slug, history=30, vintage="latest")` | `/v1/signals/{slug}` | Definition, latest, context, up to 365 recent observations |
| `p.signals.latest(slug)` | `/v1/signals/{slug}` | Just the latest observation |
| `p.signals.observations(slug, start, end, vintage="latest")` | `/v1/signals/{slug}/observations` | Any window, paged in 1,000s |
| `p.indices.list()` / `p.currents.list()` | `/v1/indices`, `/v1/currents` | Palanor Custom Indices; the Currents |
| `p.guidance.list(ticker=, metric=, since=)` | `/v1/guidance` | Guidance Ledger |
| `p.risk_factors.list(ticker=, since=, category=, change_type=)` | `/v1/risk-factors` | 10-K risk factor changes |
| `p.events.list(ticker, items=, since=, until=)` | `/v1/events` | Material events from 8-K item codes |
| `p.insiders.list(ticker, since=, codes=)` | `/v1/insiders` | Forms 3, 4, 5 |
| `p.ownership.get(ticker, period=, limit=)` | `/v1/ownership` | 13F holders and 13D/G 5% owners |
| `p.compensation.get(ticker)` | `/v1/compensation` | Pay versus performance |
| `p.fails.get(ticker, since=)` | `/v1/fails` | SEC fails-to-deliver (not short interest) |
| `p.contracts.get(ticker)` / `p.lobbying.get(ticker)` / `p.patents.get(ticker)` | `/v1/contracts`, `/v1/lobbying`, `/v1/patents` | USAspending, LDA, USPTO |
| `p.industries.sectors()` / `.search(q)` / `.get(naics)` | `/v1/industries` | US industry sizing (Census, BEA, BLS) |
| `p.release_calendar.list(start, end)` | `/v1/release-calendar` | US economic release calendar |
| `p.almanac.list()` / `p.wire.list()` | `/v1/almanac`, `/v1/wire` | Almanac posts; the Wire (Pro keys) |
| `p.get(path, **params)` | anything | Raw access to any endpoint |

Company datasets cover the S&P 500 and take a ticker (`"AAPL"`) or `cik=`.

```python
own = p.ownership.get("AAPL", limit=10)
own.to_pandas()                     # the 13F holders, largest first
own["five_percent_holders"]         # read any field like a dict
own.provenance                      # {"holders": {...}, "five_percent_holders": {...}}

events = p.events.list("MSFT", items=["1.05", "5.02"], since="2024-01-01")
events.to_pandas()[["filed_at", "item_code", "category"]]

tgt = p.guidance.list(ticker="TGT", metric="eps_adjusted")
```

## Provenance

The API serves a value only when its source may be redistributed, and it says so on the value.

- `Signal.provenance` and `Series.provenance` are a `Provenance`: `source`, `publisher`, `license_class`,
  `license_name`, `license_url`, `attribution` and `redistributable`.
- `Result.provenance` is the response's block as sent. Rows that carry their own block keep it, and
  `to_pandas()` flattens it to `provenance.source`, `provenance.license_class` and so on.
- `Result.provenance_blocks()` lists every distinct block in a response.

Show the `attribution` line wherever you display the data. Terms: [palanor.com/terms](https://palanor.com/terms).

## First-reported values

`vintage="first"` asks for each observation as first published, before revisions. Those values come from
ALFRED (Federal Reserve Bank of St. Louis), which isn't licensed for redistribution today, so the call
raises `PermissionDeniedError`. They are shown in the Palanor Terminal.

## Errors, retries and your allowance

Every call counts against the key's monthly allowance. `result.rate_limit` (and `p.last_rate_limit`) show
the tier, the cap and what's left.

| Exception | When |
|---|---|
| `AuthenticationError` | No key, a wrong key, or a revoked key (401) |
| `PermissionDeniedError` | The tier doesn't include the endpoint, or the data can't be redistributed (402, 403) |
| `NotFoundError` | No such signal or company, or a signal whose source is display-only (404) |
| `BadRequestError` | A malformed parameter (400) |
| `QuotaExceededError` | The month's cap is spent (429). Not retried. |
| `RateLimitError` | A transient 429 that outlasted the retries |
| `ServerError` / `APIConnectionError` | 5xx, or the API couldn't be reached |

Transient failures are retried up to `max_retries` times (default 3). The wait doubles from `backoff`
seconds (default 0.5) with jitter, and a `Retry-After` header is honoured.

```python
p = Palanor(max_retries=5, backoff=1.0, timeout=60)
```

## Development

```bash
cd packages/palanor-python
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"
pytest -m "not live"                       # offline: transport, retries, paging, DataFrames
PALANOR_API_KEY=plnr_... pytest -m live    # the live API, about 30 requests
mypy                                       # strict
```

MIT licensed (the client code). The data is governed by [palanor.com/terms](https://palanor.com/terms)
and by the license in each result's provenance block.
