Metadata-Version: 2.4
Name: hub-equity
Version: 2.0.0
Summary: Hub-Equity Python SDK: programmatic access to XBRL / iXBRL financial data (SEC EDGAR + ESEF Europe) via the public REST API.
Author-email: Hub-Equity <contact@hub-equity.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://hub-equity.com
Project-URL: Documentation, https://docs.hub-equity.com
Keywords: xbrl,ixbrl,financial-data,sec-edgar,esef,ifrs,us-gaap
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
License-File: NOTICE
Requires-Dist: httpx<1.0,>=0.25
Requires-Dist: pydantic>=2
Provides-Extra: pandas
Requires-Dist: pandas>=2.0; extra == "pandas"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
Requires-Dist: pytest-cov>=4; extra == "dev"
Requires-Dist: respx>=0.21; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Dynamic: license-file

# hub-equity: Python SDK

Typed Python client for the **Hub-Equity** financial-data API: machine-readable
**XBRL / iXBRL** from primary filings, **SEC EDGAR** (US) and **ESEF** (Europe),
normalized into canonical financial concepts, with the **provenance preserved**.

What sets the data apart, and what this SDK exposes:

- **Source-linked.** Every value carries the filing it came from (`form_type`,
  `filing_date`, a viewer URL). You can trace any number back to the filer's
  document, not a black box that silently rewrites as-filed history.
- **XBRL-native.** Calculation linkbase (with filer weights *and* verifier-corrected
  weights), dimensional structure, and the raw filer-declared facts, not just
  flattened labels.
- **Restatement-aware.** Per-concept deltas across 10-K/A amendments and across
  silent restatements, the figures a later filing reprinted differently with no
  amendment filed.
- **Cross-issuer, calendar-aligned.** `compare` matches periods by calendar year
  (Bloomberg / FactSet convention), so Apple (Sep FYE) and a Dec-FYE peer line up.

The SDK is a thin, typed wrapper over the public REST API: it ships **no data and
no secrets**; you bring your own API key. Full API reference: <https://docs.hub-equity.com>.
It covers a subset of the API: fact decomposition, segments, the amendment summary,
cross-period comparison, roll-up, currency conversion, the entity quality grade,
validation results, extension concepts and the hub catalog are read over REST.

> **Maturity.** The data engine and public REST API behind this SDK run in
> production and power Hub-Equity's own chat. This PyPI package is a newly
> published client for that API; the surface is stable (Semantic Versioning, 2.x), but as a
> distributed package it is fresh, hence the Beta classifier.

## Install

```bash
pip install hub-equity
# Optional pandas helpers:
pip install 'hub-equity[pandas]'
```

Python 3.10+. Runtime deps: `httpx`, `pydantic`.

## Authentication

```python
from hub_equity import HubEquity

# Explicit key
client = HubEquity(api_key="INSERT_YOUR_API_KEY")

# Or set HUBEQUITY_API_KEY env var (recommended for notebooks)
client = HubEquity()
```

A key is required: the client refuses to start without one (`ValueError`), since the API answers `401 AUTH_REQUIRED` to a call without a key. A Free key takes a minute at <https://hub-equity.com/settings/api-keys> (shown once, store it securely).
Set `HUBEQUITY_API_URL` to point the client at a non-default host (defaults to
`https://api.hub-equity.com`).

**Entity arguments accept a Hub-Equity UUID, a ticker (e.g. `"AAPL"`) or
`TICKER.MIC` (e.g. `"MC.XPAR"`)** on every entity method, `compare` included. An
ambiguous ticker answers `409` with the suffixes that disambiguate it.

## Quickstart

```python
from hub_equity import HubEquity

with HubEquity() as client:
    fin = client.get_financials("MC.XPAR", years=5)          # LVMH, every statement
    revenue = fin.metric("REVENUE")
    print([(v.fiscal_year, v.display_value) for v in revenue.values])

    rev = client.get_metric_history("AAPL", "REVENUE", years=5)
    print(f"{rev.entity_name}: {rev.hub_label} (CAGR {rev.cagr_pct:.1f}%)")
    for p in rev.points:
        print(f"  FY{p.fiscal_year}: {p.value:,.0f} {p.currency}"
              f"  ({p.yoy_growth_pct:+.1f}% YoY)  ← {p.source.form_type} {p.source.filing_date}")
        # p.source.viewer_url / p.source.external_url -> trace back to the filing
```

## Methods

| Area | Method | Returns |
|---|---|---|
| **Entities** | `find_entity(query)` | `FindEntityResult`: up to 5 ranked matches (name, ticker, `TICKER.MIC`, CIK) |
| | `search_entities(q, ...)`, `iter_entities(q, ...)` | `SearchResponse` / generator of `SearchResult` |
| | `get_entity_profile(entity)` | `EntityProfile`: identifiers, DEI, auditor, latest filings |
| **Statements** | `get_financials(entity, *, years=3, category='all', currency=None, end_year=None)` | `Financials`: periods + metrics; cite `MetricValue.display_value` |
| **Facts** | `get_fact(entity, code, fiscal_year, fiscal_period_type='FY')` | `Fact`: one value + its source filing (`HubEquityError` 404 when none is served) |
| **Screener** | `screen_companies(*, country, sector, min_revenue, ..., sort_by, limit=20)` | `ScreenCompaniesResult` (Free: 20 rows, no quality filter) |
| **Time-series** | `get_metric_history(entity, code, *, years=5, period_type='FY')` | `MetricHistory`: values + YoY + CAGR |
| **As-of** | `get_metric_as_of(entity, code, as_of, *, years=5, period_type='FY')` | `AsOfResponse`: series read from the filings filed on or before a date (beta: can differ from the served values) |
| **Compare** | `compare(entity_ids, hub_concept_codes, fiscal_year, fiscal_period_type='FY')` | `CompareEntitiesResult`: N×M matrix |
| **Calc linkbase** | `get_calculation_sections(filing_id)` | `CalculationSectionsResponse`: calc roles |
| | `get_calculation_tree(filing_id, link_role)` | `CalculationTree`: edges + weights |
| **Dimensions** | `get_entity_dimensions(entity)` | `EntityDimensionsResponse`: axes + members |
| **Raw facts** | `get_filing_facts(filing_id, *, section, concept, is_extension, limit, offset)`, `iter_filing_facts(filing_id, *, ..., per_page=200)` | `RawFactsResponse` / generator of `RawFact` (Free: structure without the values) |
| **Restatements** | `get_restatement_diff(entity, *, fiscal_year, hub_concept_code, min_diff_pct=0.01, kind, source='amendments')` | `AmendmentDiffResult` (`source='comparatives'`: silent restatements) |
| **Quality** | `get_filing_quality_grade(filing_id)` | `QualityGradeResponse`: A-F + 4 drivers |
| **Concepts** | `search_concepts(q, *, taxonomy, category, limit, offset, cursor)` | `ConceptSearchResponse` |
| | `iter_concepts(q, *, taxonomy, category, page_size=100)` | generator of `ConceptSummary` |
| | `get_concept(hub_concept_code)` | `ConceptDetail`: forward catalog entry (label, description, category, taxonomy scope) |
| **Exports** (Team, Enterprise) | `create_export(entity_ids, *, years=3, category='all', xbrl='none')`, `list_exports`, `get_export(job_id)` | `ExportJob` |
| | `download_export(job_id, path=None)` | zip `bytes`, or the `Path` written |
| **Webhooks** (Enterprise) | `create_webhook(name, url, *, events, entity_ids)`, `rotate_webhook_secret(id)` | `WebhookCreated` (secret shown once) |
| | `list_webhooks`, `get_webhook(id)`, `update_webhook(id, ...)`, `delete_webhook(id)` | `Webhook` |
| | `list_webhook_deliveries(id, *, limit=50)` | `list[WebhookDelivery]` |
| | `send_webhook_test(id)`, `redeliver_webhook_delivery(id, delivery_id)` | `WebhookAttempt` |

Every method returns a typed `pydantic` model (autocomplete + validation), with
`extra="allow"` so a newer API never breaks an older SDK.

### Compare issuers (calendar-year aligned, premium)

```python
matrix = client.compare(
    entity_ids=["AAPL", "MSFT"],
    hub_concept_codes=["REVENUE", "NET_INCOME"],
    fiscal_year=2024,
)
for row in matrix.rows:
    cell = row.cells.get("REVENUE")
    if cell:
        print(f"{row.name} ({row.ticker}): {cell.value:,.0f} {cell.currency}"
              f"  [{cell.source.form_type if cell.source else ''}]")
```

### Audit a number to its filing

```python
# Drill to the raw filer-declared facts (qname, unit, decimals, dimensions).
# The values come on a paid plan; a Free key reads the structure, values null.
facts = client.get_filing_facts("<filing-uuid>", concept="Revenue", limit=50)
for f in facts.facts:
    print(f.concept_qname, f.value_numeric, f.unit, f.dimensions)
```

### pandas

```python
import pandas as pd

hist = client.get_metric_history("MSFT", "REVENUE", years=10)
df = pd.DataFrame([p.model_dump() for p in hist.points])
df["source_form"] = [p.source.form_type for p in hist.points]
```

### Background exports (Team, Enterprise)

```python
import time

job = client.create_export(["AAPL", "MC.XPAR"], years=5)
while job.status in ("pending", "running"):
    time.sleep(10)
    job = client.get_export(job.id)
if job.status == "ready":
    client.download_export(job.id, path="export.zip")  # follows the 307 to the signed URL
else:
    print(job.error)
```

## Webhooks (Enterprise, SDK 2.0.0)

```python
hook = client.create_webhook("prod", "https://example.com/hub-equity")
SECRET = hook.secret                      # shown once: store it now
client.send_webhook_test(hook.id)         # a signed ping, answered with your endpoint's status
client.list_webhook_deliveries(hook.id)   # what was sent, skipped or retried
```

A subscription created in `/settings/webhooks` (or `create_webhook`) delivers signed
`POST`s to your endpoint: `filing.ingested` when a filing's values are served,
`restatement.material` when a later filing restates a value you may have used.
Verify the raw body, not a re-serialised parse:

```python
from hub_equity import verify_webhook_signature

@app.post("/hub-equity")
async def receive(request):
    raw = await request.body()
    if not verify_webhook_signature(SECRET, request.headers, raw):
        return Response(status_code=401)
    event = json.loads(raw)          # {"id", "event", "created_at", "data"}
    ...
    return Response(status_code=204)  # answer 2xx within 10 s
```

The check is constant-time, rejects a timestamp more than five minutes from your
clock, and accepts either signature during the 24 hours that follow a secret
rotation (`v1=<new>,v1=<old>`). Deduplicate on the `X-Hub-Equity-Delivery` header:
delivery is at-least-once.

## Reliability

- Retries `429` (rate limit) + `5xx` (server) with exponential backoff
  (3 retries, base 1 s, cap 10 s); honors `Retry-After` when present.
- After exhausting retries on `429`, raises `RateLimitError` (subclass of
  `HubEquityError`) with the last `retry_after` attached. A `429` whose
  `Retry-After` exceeds the 10 s cap (a daily or monthly allowance used up) is
  raised at once, not retried.
- Other 4xx raise `HubEquityError` immediately (no retries), with `.status_code`.
- The exception message carries the API's own `error.code` and `error.message`
  (e.g. which suffix disambiguates a ticker, when a quota resets).

```python
from hub_equity import HubEquity, HubEquityError, RateLimitError

with HubEquity() as client:
    try:
        rev = client.get_metric_history("AAPL", "REVENUE")
    except RateLimitError as exc:
        print(f"Rate-limited, retry after {exc.retry_after}s")
    except HubEquityError as exc:
        print(f"{exc.status_code}: {exc}")
```

## Rate limits

| Caller | Per minute |
|---|---|
| Free key | 120 |
| Builder key | 300 |
| Team key | 600 |
| Enterprise key | 1 000 |

Bucketed per API key.

Volume allowances are counted per workspace on API keys and refuse with a
`429` (`DAILY_QUOTA_EXCEEDED` / `MONTHLY_QUOTA_EXCEEDED`, headers `X-Quota-*`,
`resets_on` in the body): Free 200 calls a day and 5 000 a month, Builder
50 000 a month, Team 500 000 a month, Enterprise unlimited.

Premium endpoints (`compare`, `compare-filings`, as-of, dimensions,
restatement diff, calculation tree, validation results,
extension concepts) require a paid key (`403 SCOPE_MISSING` on a Free key,
`401` without a key). A Free key is also capped on `/decomposition` (`depth=1`)
and on `/companies/screen` (20 results, no `min_quality_grade`), gets the axes of
`/segments/{code}` without their members, and reads the raw facts of
`/filings/{id}/facts` without their values.

## Versioning

The SDK follows Semantic Versioning: a breaking change ships only in a major
version. Pin a major version in production (`hub-equity>=2,<3`). Response models
use `extra="allow"`, so additive API changes ship in minor releases without
breaking older SDK installs.

**2.0.0 breaks two things**, because the API itself changed: `HubEquity()`
without a key raises `ValueError` (the API refuses keyless calls, so the
`api_key=""` examples of 1.0 and 1.1 no longer run), and
`CompareCell.extraction_method` is removed. See the CHANGELOG.

## License

Apache-2.0. See [LICENSE](LICENSE).
