Metadata-Version: 2.5
Name: arche-sdk
Version: 0.2.1
Summary: Official Python SDK for Arche public data APIs
Project-URL: Homepage, https://arche.fi
Project-URL: Documentation, https://docs.arche.fi/sdks/python
Author: Arche
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: api,arche,edgar,finance,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: httpx<1,>=0.27
Requires-Dist: pydantic<3,>=2.7
Provides-Extra: dev
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Description-Content-Type: text/markdown

# Arche Python SDK

Typed, production-grade Python SDK for Arche public data APIs.

This initial release covers the primary public data surface:
- Companies
- Filings
- Statements
- Narratives
- Bulk export (Scale and Enterprise plans)

It does not include protected control-plane, billing, org/team/project/environment admin, usage, webhook, MCP, or internal ops routes.

## Installation

```bash
pip install arche-sdk
```

For local monorepo usage:

```bash
pip install -e sdks/python
```

## Authentication

### API key

```python
from arche_sdk import ArcheClient

client = ArcheClient(api_key="your_api_key")
```

### Bearer token for protected portal/control-plane routes

```python
from arche_sdk import ArcheClient

client = ArcheClient(bearer_token="your_jwt_or_token")
```

Use bearer auth only for protected portal/control-plane or internal routes that
explicitly require it. Public runtime API examples and quickstarts should use
`api_key`.

`api_key` and `bearer_token` are mutually exclusive; passing both raises
`ValueError`.

## Quickstart

### Companies

```python
from arche_sdk import ArcheClient

with ArcheClient(api_key="...") as client:
    company = client.companies.get("0000320193")
    resolved = client.companies.resolve("AAPL")
    completeness = client.companies.get_completeness("0000320193")
```

### Naming a company

Every method that takes a `cik` also accepts a ticker symbol in its place, and a
`ciks` list accepts tickers. Digits are sent as a CIK and anything else as a
ticker, which the API resolves to the company whose filings hold its statements
on the request's date; one list must hold CIKs only or tickers only. A
`company_id` (narratives) takes the company UUID, the CIK or the ticker.

```python
with ArcheClient(api_key="...") as client:
    versions = client.statements.list("AAPL", statement_type="INCOME_STATEMENT")
    derived = client.statements.get_derived_metrics_time_series(
        ciks=["AAPL", "MSFT"],
        statement_type="INCOME_STATEMENT",
        metrics=["GROSS_MARGIN"],
        frequency="annual",
    )
```

### Filings

```python
with ArcheClient(api_key="...") as client:
    page = client.filings.list("0000320193", filing_types=["10-K"], page=1, page_size=20)
    filing = client.filings.get("0000320193", "0000320193-24-000001")
    statements = client.filings.list_statements("0000320193", "0000320193-24-000001")
```

### Statements

```python
with ArcheClient(api_key="...") as client:
    versions = client.statements.list("0000320193", statement_type="INCOME_STATEMENT")
    timeline = client.statements.get_restatement_timeline(
        "0000320193", "INCOME_STATEMENT", 2024, "FY"
    )
    derived = client.statements.get_derived_metrics_time_series(
        ciks=["0000320193"],
        statement_type="INCOME_STATEMENT",
        metrics=["GROSS_MARGIN"],
        frequency="annual",
    )

    # As known at a date, only the statements asked for: here the FY2024
    # income statement. snapshot_hash still names the full as-of selection.
    as_of = client.statements.get_as_of_financials_page(
        "2025-03-31",
        "AAPL",
        limit=10,
        statement_type="INCOME_STATEMENT",
        fiscal_year_from=2024,
        fiscal_year_to=2024,
        fiscal_period="FY",
    )
```

### Narratives

```python
with ArcheClient(api_key="...") as client:
    narratives = client.narratives.list(
        "11111111-1111-1111-1111-111111111111",
        as_of="2024-12-31",
        narrative_type="CHANGE_SUMMARY",
    )
```

### Bulk export

Scale and Enterprise plans can take a complete local copy of the served corpus
as Parquet files instead of paging the API. `sync()` fetches only what changed:

```python
with ArcheClient(api_key="...") as client:
    result = client.bulk.sync("./arche-corpus")

    print(result.snapshot_id, len(result.downloaded), "downloaded")
    print(len(result.skipped), "already held")
```

Download URLs are issued under the bulk-data license, which the organization
accepts once per license version. Until it has, `sync()` raises
`BulkLicenseNotAcceptedError`. Read the license and accept it from the SDK; the
person named confirms they may bind the organization:

```python
with ArcheClient(api_key="...") as client:
    current = client.bulk.license()
    print(current.license_version, current.accepted)
    if not current.accepted:
        print(current.text)
        client.bulk.accept_license(name="Jane Doe", email="jane@example.com")
```

`accept_license()` accepts the version and SHA-256 that `license()` returns, so
the acceptance is of the text served at that moment. Accepting a version the
organization already accepted returns the existing record unchanged.

Run it again tomorrow against the same directory and it transfers only the
partitions whose bytes moved. A file is skipped when the local copy already
hashes to the `sha256` the snapshot's manifest records, so the skip is a
statement about the bytes on disk rather than about a timestamp, and an
interrupted sync resumes correctly. Digests are remembered in
`<destination>/.arche-bulk-sync.json` so a second sync does not reread
gigabytes to prove nothing changed; a file whose size or modification time
differs from the record is rehashed rather than trusted, and `rehash=True`
ignores the record entirely.

Restrict the sync, or watch it:

```python
def show(event):
    print(f"[{event.index}/{event.total}] {event.action} {event.path}")

with ArcheClient(api_key="...") as client:
    client.bulk.sync("./arche-corpus", datasets=["statements", "financials"], progress=show)
```

Read the contract, browse snapshots, or fetch one file:

```python
with ArcheClient(api_key="...") as client:
    for dataset in client.bulk.datasets():
        print(dataset.dataset, dataset.partition_column, [c.name for c in dataset.columns])

    manifest = client.bulk.snapshot()          # "latest" by default
    print(manifest.snapshot_id, manifest.read_at, manifest.bytes)

    changed = [item.path for item in manifest.files if item.changed]
    client.bulk.download_file(manifest.snapshot_id, changed[0], "./arche-corpus")
```

Every download is verified against the manifest's checksum and raises
`ChecksumMismatchError` on a mismatch, removing the partial file. A manifest
path that would write outside the destination is refused rather than resolved. The URLs are
presigned and carry their own credentials: the SDK fetches them without an API
key, and each one is charged its file's full size against the organization's
daily download allowance when it is issued, not when the bytes move. Past the
allowance the API answers `RateLimitError` with code
`BULK_DOWNLOAD_BUDGET_EXCEEDED`.

Read the files with any Parquet reader. Partition directories are named
`fiscal_year_2025`, not Hive's `fiscal_year=2025`, because the files already
carry the partition as a column:

```python
import duckdb

duckdb.sql("SELECT * FROM './arche-corpus/financials/*/*.parquet' LIMIT 5").show()
```

## Agent tools

`arche_sdk.tools.TOOLS` defines four tools an agent can call: resolve a
ticker, list a company's statements, read them as they were known on a past
date (`client.as_of.financials`), and verify an earlier answer by its replay
token (`client.query.verify`). Each has a name, a description and a Pydantic
argument model, and `tool.call(client, **arguments)` returns JSON-ready data.

The same four tools ship for agent frameworks as `langchain-arche`,
`llama-index-tools-arche` and `crewai-arche`, each exposing
`arche_tools(api_key=...)`.

## Pagination

List endpoints return `Page[T]` with `page`, `page_size`, `total`, and `items`.

```python
with ArcheClient(api_key="...") as client:
    first_page = client.filings.list("0000320193", page=1, page_size=50)
    if first_page.has_next:
        second_page = client.filings.list("0000320193", page=2, page_size=50)

    for filing in client.filings.iter_all("0000320193", page_size=100):
        print(filing.accession_id)
```

## Error handling

```python
from arche_sdk import ArcheClient
from arche_sdk.errors import APIError, RateLimitError

try:
    with ArcheClient(api_key="...") as client:
        client.companies.get("0000000000")
except RateLimitError as exc:
    print(exc.retry_after, exc.rate_limit_remaining)
except APIError as exc:
    print(exc.status_code, exc.code, exc.message, exc.trace_id, exc.request_id)
```

## Request ID and trace ID

- The SDK sends `X-Request-ID` on every request (auto-generated when missing).
- On API errors, the SDK exposes:
  - `request_id` from response `X-Request-ID`
  - `trace_id` from canonical error envelope (`error.trace_id`)

## License

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