Metadata-Version: 2.4
Name: qtsurfer-api-client
Version: 0.98.0
Summary: Auto-generated Python client for the QTSurfer API.
Project-URL: Homepage, https://github.com/QTSurfer/api-client-python
Project-URL: Repository, https://github.com/QTSurfer/api-client-python
Project-URL: Issues, https://github.com/QTSurfer/api-client-python/issues
Project-URL: OpenAPI Spec, https://github.com/QTSurfer/qtsurfer-api
Project-URL: SDK (Java), https://github.com/QTSurfer/sdk-java
Project-URL: SDK (TypeScript), https://github.com/QTSurfer/sdk-ts
Author-email: Manuel Polo <mrmx@users.noreply.github.com>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: api,backtest,client,openapi,qtsurfer,trading
Classifier: Development Status :: 3 - Alpha
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.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Office/Business :: Financial :: Investment
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: attrs>=22.2.0
Requires-Dist: httpx<1,>=0.23
Requires-Dist: python-dateutil>=2.8.0
Description-Content-Type: text/markdown

<h1 align="center">QTSurfer API Client · Python</h1>

<p align="center">
  <a href="https://github.com/QTSurfer/api-client-python/actions/workflows/ci.yml"><img src="https://github.com/QTSurfer/api-client-python/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
  <a href="https://pypi.org/project/qtsurfer-api-client/"><img src="https://img.shields.io/pypi/v/qtsurfer-api-client.svg" alt="PyPI"></a>
  <a href="https://pypi.org/project/qtsurfer-api-client/"><img src="https://img.shields.io/pypi/pyversions/qtsurfer-api-client.svg" alt="Python versions"></a>
  <a href="LICENSE"><img src="https://img.shields.io/badge/License-Apache%202.0-blue.svg" alt="License"></a>
</p>

<p align="center">
  Auto-generated Python client for the <a href="https://github.com/QTSurfer/qtsurfer-api">QTSurfer API</a>, built from the OpenAPI 3.1 spec with <a href="https://github.com/openapi-generators/openapi-python-client">openapi-python-client</a> on top of <a href="https://www.python-httpx.org/">httpx</a>.
</p>

<p align="center">
  <code>pip install qtsurfer-api-client</code>
</p>

---

Intentionally thin: one function per endpoint, 1:1 with the spec. For workflow orchestration (polling, retries, domain objects, unified errors), use [`qtsurfer-sdk`](https://github.com/QTSurfer/sdk-python) (coming soon).

- **Sync-first, httpx-powered** — same call site shape as the Java/TS siblings.
- **Spec-driven** — generated sources fetched from [`QTSurfer/qtsurfer-api`](https://github.com/QTSurfer/qtsurfer-api) by `scripts/regenerate.sh`.
- **Fully typed** — `py.typed` marker, `mypy --strict` clean (consumers see precise types for every request/response).
- **Python 3.11+** — modern type hints, no compat shims.

## Installation

```bash
pip install qtsurfer-api-client
```

Or with [uv](https://docs.astral.sh/uv/):

```bash
uv add qtsurfer-api-client
```

## Quick start

```python
import os

from qtsurfer.api.client import AuthenticatedClient
from qtsurfer.api.client.api.exchange import get_exchanges, get_instruments

client = AuthenticatedClient(
    base_url="https://api.qtsurfer.com/v1",
    token=os.environ["QTSURFER_TOKEN"],
)

exchanges = get_exchanges.sync(client=client)
for ex in exchanges or []:
    print(ex.id, ex.name)

instruments = get_instruments.sync(client=client, exchange_id="binance")
print(f"{len(instruments.data)} instruments on binance ({instruments.meta.segment.value})")
```

### API key → JWT

Every endpoint above expects a short-lived JWT in the `Authorization: Bearer …`
header. Exchange a long-lived API key for one via `auth`:

```python
import os

from qtsurfer.api.client import AuthenticatedClient
from qtsurfer.api.client.api.auth import auth

# AuthenticatedClient also drives the apikey header — set prefix="" so it
# sends `X-API-Key: <key>` instead of `Authorization: Bearer <key>`.
apikey_client = AuthenticatedClient(
    base_url="https://api.qtsurfer.com/v1",
    token=os.environ["QTSURFER_APIKEY"],
    prefix="",
    auth_header_name="X-API-Key",
)

token_response = auth.sync(client=apikey_client)
jwt = token_response.access_token  # use this in subsequent calls
```

For production use, prefer the [`qtsurfer-sdk`](https://github.com/QTSurfer/sdk-python)
`auth(apikey)` helper — it handles token refresh, env-var pickup
(`QTSURFER_APIKEY`), and pluggable token storage so callers don't reinvent any
of it on top of the raw client.

Each generated endpoint module exposes four entrypoints:

| Function | Returns |
| --- | --- |
| `sync(...)` | parsed model (or `None` on a defined error response) |
| `sync_detailed(...)` | full `Response[...]` (status, headers, parsed, content) |
| `asyncio(...)` | parsed model, awaitable |
| `asyncio_detailed(...)` | full `Response[...]`, awaitable |

## API surface

| Module | Operation | Method · Path |
| --- | --- | --- |
| `api.auth` | `auth` | `POST /auth/token` — exchange API key for a short-lived JWT |
| `api.exchange` | `get_exchanges` | `GET /exchanges` |
| `api.exchange` | `get_instruments` | `GET /exchange/{exchangeId}/instruments` (default `spot` segment) |
| `api.exchange` | `get_segment_instruments` | `GET /exchange/{exchangeId}/{segment}/instruments` |
| `api.exchange` | `get_exchange_tickers_hour` | `GET /exchange/{exchangeId}/tickers/{base}/{quote}` |
| `api.exchange` | `get_exchange_klines_hour` | `GET /exchange/{exchangeId}/klines/{base}/{quote}` |
| `api.strategy` | `get_strategy_status` | `GET /strategy/{strategyId}` |
| `api.backtesting` | `prepare_backtesting` | `POST /backtesting/prepare` |
| `api.backtesting` | `get_preparation_status` | `GET /backtesting/prepare/{jobId}` |
| `api.backtesting` | `execute_backtesting` | `POST /backtesting/execute` |
| `api.backtesting` | `cancel_execution` | `POST /backtesting/execute/{jobId}/cancel` |
| `api.backtesting` | `get_execution_result` | `GET /backtesting/execute/{jobId}` |

> Exact module/function names are produced from `operationId` in the OpenAPI spec. Run `scripts/regenerate.sh` to refresh and check `src/qtsurfer/api/client/_generated/api/` for the authoritative listing.

All generated model types (`Exchange`, `InstrumentDetail`, `InstrumentCoverage`, `CoverageWindow`, `JobState`, `PrepareJobState`, `BacktestJobResult`, `ResultMap`, `ResponseError`, …) live under `qtsurfer.api.client.models`. `get_instruments`/`get_segment_instruments` return an `InstrumentListResponse` (HAL envelope: `data` + `meta` + `_links`), not a bare list — each `InstrumentDetail.coverage` carries per-data-type `CoverageWindow`s instead of flat `dataFrom`/`dataTo`. A single-instrument `get_preparation_status` returns a `PrepareJobState` — always terminal (`status: Completed`), with a `coverage_ratio` and a per-hour `hours_without_data` breakdown to act on instead of polling.

> **`POST /strategy` (`postStrategy`)** is currently omitted by the generator because the spec declares its request body as `text/plain` and `openapi-python-client` only emits JSON / form / multipart bodies. Call it directly via the underlying `httpx` client (`client.get_httpx_client().post("/strategy", content=src, headers={"Content-Type": "text/plain"})`) until the spec is restructured.

### Binary downloads (`/exchange/{ex}/tickers|klines/{base}/{quote}`)

These endpoints return raw [Lastra](https://github.com/QTSurfer/lastra-java) bytes (default) or Parquet (`format=parquet`). The generated `sync()` helpers parse the response body as JSON and will raise on binary payloads; use `sync_detailed()` and read `response.content` directly:

```python
from qtsurfer.api.client import AuthenticatedClient
from qtsurfer.api.client.api.exchange import get_exchange_tickers_hour

client = AuthenticatedClient(base_url="https://api.qtsurfer.com/v1", token=token)

response = get_exchange_tickers_hour.sync_detailed(
    client=client,
    exchange_id="binance",
    base="BTC",
    quote="USDT",
    hour="2026-01-15T10",
)
with open("BTC_USDT_2026-01-15_h10.lastra", "wb") as f:
    f.write(response.content)
```

For very large segments, drop down to the underlying `httpx.Client` (`client.get_httpx_client()`) and stream:

```python
with client.get_httpx_client().stream(
    "GET",
    "/exchange/binance/klines/BTC/USDT",
    params={"hour": "2026-01-15T10", "format": "parquet"},
) as r:
    r.raise_for_status()
    with open("out.parquet", "wb") as f:
        for chunk in r.iter_bytes():
            f.write(chunk)
```

## Configuring the client

Both `Client` and `AuthenticatedClient` accept the standard hooks of the upstream generator:

```python
from qtsurfer.api.client import AuthenticatedClient

client = AuthenticatedClient(
    base_url="https://api.qtsurfer.com/v1",
    token=token,
    timeout=httpx.Timeout(30.0),
    verify_ssl=True,
    headers={"X-Request-Id": "..."},
    raise_on_unexpected_status=True,
)
```

Need per-call customisation (e.g. swap the underlying `httpx.Client` for one with a custom transport)? Use `client.with_httpx_client(my_httpx_client)` or `client.set_httpx_client(...)`.

## Regenerating the client

The `src/qtsurfer/api/client/_generated/` directory is a committed build artifact produced from the OpenAPI spec hosted at [`QTSurfer/qtsurfer-api`](https://github.com/QTSurfer/qtsurfer-api/blob/main/openapi.yaml). Never hand-edit it.

```bash
uv sync                    # install pinned dev deps
./scripts/regenerate.sh    # fetch spec + regenerate + sync pyproject version
uv run ruff check src/ tests/
uv run mypy src/
uv run pytest -v
```

Generator configuration lives in `codegen.config.yaml`. The spec URL is hard-coded in `scripts/regenerate.sh`; point it at a tag/commit for fully reproducible builds.

## Development

| Command | Description |
| ------ | ----------- |
| `uv sync` | Install dependencies from `uv.lock` |
| `./scripts/regenerate.sh` | Re-fetch spec + regenerate client |
| `uv run ruff check src/ tests/` | Lint |
| `uv run ruff format src/ tests/` | Format |
| `uv run mypy src/` | Type-check (`--strict`) |
| `uv run pytest -v` | Run tests |
| `uv run python -m build` | Build wheel + sdist into `dist/` |

## Versioning

`pyproject.toml`'s `version` field is kept in lockstep with the `info.version` of the OpenAPI spec by `scripts/regenerate.sh`. Tags pushed to `main` (`vX.Y.Z`) trigger the PyPI publish workflow via OIDC trusted publishing.

## License

Apache-2.0 — see [LICENSE](./LICENSE).
