Auto-generated Python client for the QTSurfer API, built from the OpenAPI 3.1 spec with openapi-python-client on top of httpx.
pip install qtsurfer-api-client
Intentionally thin: one function per endpoint, 1:1 with the spec. For workflow orchestration (polling, retries, domain objects, unified errors), use qtsurfer-sdk (coming soon).
- Sync-first, httpx-powered — same call site shape as the Java/TS siblings.
- Spec-driven — generated sources fetched from
QTSurfer/qtsurfer-apibyscripts/regenerate.sh. - Fully typed —
py.typedmarker,mypy --strictclean (consumers see precise types for every request/response). - Python 3.11+ — modern type hints, no compat shims.
pip install qtsurfer-api-clientOr with uv:
uv add qtsurfer-api-clientimport os
from qtsurfer.api.client import AuthenticatedClient
from qtsurfer.api.client.api.exchange import list_exchanges, list_instruments
client = AuthenticatedClient(
base_url="https://api.qtsurfer.com/v1",
token=os.environ["QTSURFER_TOKEN"],
)
exchanges = list_exchanges.sync(client=client)
for ex in exchanges or []:
print(ex.id, ex.name)
instruments = list_instruments.sync(client=client, exchange_id="binance")
print(f"{len(instruments.data)} instruments on binance ({instruments.meta.segment.value})")Every endpoint above expects a short-lived JWT in the Authorization: Bearer …
header. Exchange a long-lived API key for one via authenticate:
import os
from qtsurfer.api.client import AuthenticatedClient
from qtsurfer.api.client.api.auth import authenticate
# 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 = authenticate.sync(client=apikey_client)
jwt = token_response.access_token # use this in subsequent callsFor production use, prefer the qtsurfer-sdk
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 |
| Module | Operation | Method · Path |
|---|---|---|
api.auth |
authenticate |
POST /auth/token — exchange API key for a short-lived JWT |
api.exchange |
list_exchanges |
GET /exchanges |
api.exchange |
list_instruments |
GET /exchange/{exchangeId}/instruments (default spot segment) |
api.exchange |
list_segment_instruments |
GET /exchange/{exchangeId}/{segment}/instruments |
api.exchange |
download_tickers |
GET /exchange/{exchangeId}/tickers/{base}/{quote} |
api.exchange |
download_klines |
GET /exchange/{exchangeId}/klines/{base}/{quote} |
api.strategy |
get_strategy |
GET /strategy/{strategyId} |
api.strategy |
validate_strategy |
POST /strategy/{strategyId}/validate |
api.backtesting |
prepare_backtest |
POST /backtesting/prepare |
api.backtesting |
get_prepare_status |
GET /backtesting/prepare/{jobId} |
api.backtesting |
execute_backtest |
POST /backtesting/execute |
api.backtesting |
cancel_backtest |
POST /backtesting/execute/{jobId}/cancel |
api.backtesting |
get_backtest_result |
GET /backtesting/execute/{jobId} |
Exact module/function names are produced from
operationIdin the OpenAPI spec. Runscripts/regenerate.shto refresh and checksrc/qtsurfer/api/client/_generated/api/for the authoritative listing.
All generated model types (Exchange, InstrumentDetail, InstrumentCoverage, CoverageWindow, JobState, PrepareJobState, StrategyState, BacktestJobResult, ResultMap, ResponseError, …) live under qtsurfer.api.client.models. list_instruments/list_segment_instruments return an InstrumentListResponse (HAL envelope: data + meta + _links), not a bare list — each InstrumentDetail.coverage carries per-data-type CoverageWindows instead of flat dataFrom/dataTo. A single-instrument get_prepare_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. get_strategy returns a StrategyState, whose validation field (not_validated / pending / passed / failed) reports the outcome of the most recent validate_strategy check rather than a compile job status.
POST /strategy(compileStrategy) is currently omitted by the generator because the spec declares its request body astext/plainandopenapi-python-clientonly emits JSON / form / multipart bodies. Call it directly via the underlyinghttpxclient (client.get_httpx_client().post("/strategy", content=src, headers={"Content-Type": "text/plain"})) until the spec is restructured.get_strategyandvalidate_strategyhave no such restriction and generate normally.
These endpoints return raw Lastra 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:
from qtsurfer.api.client import AuthenticatedClient
from qtsurfer.api.client.api.exchange import download_tickers
client = AuthenticatedClient(base_url="https://api.qtsurfer.com/v1", token=token)
response = download_tickers.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:
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)Both Client and AuthenticatedClient accept the standard hooks of the upstream generator:
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(...).
The src/qtsurfer/api/client/_generated/ directory is a committed build artifact produced from the OpenAPI spec hosted at QTSurfer/qtsurfer-api. Never hand-edit it.
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 -vGenerator 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.
| 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/ |
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.
Apache-2.0 — see LICENSE.