siftingio 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- siftingio-0.1.0/.gitignore +10 -0
- siftingio-0.1.0/CHANGELOG.md +21 -0
- siftingio-0.1.0/CLAUDE.md +89 -0
- siftingio-0.1.0/LICENSE +21 -0
- siftingio-0.1.0/Makefile +72 -0
- siftingio-0.1.0/PKG-INFO +198 -0
- siftingio-0.1.0/README.md +165 -0
- siftingio-0.1.0/examples/rest.py +48 -0
- siftingio-0.1.0/examples/websocket.py +47 -0
- siftingio-0.1.0/pyproject.toml +75 -0
- siftingio-0.1.0/src/siftingio/__init__.py +30 -0
- siftingio-0.1.0/src/siftingio/_transport.py +249 -0
- siftingio-0.1.0/src/siftingio/client.py +143 -0
- siftingio-0.1.0/src/siftingio/errors.py +52 -0
- siftingio-0.1.0/src/siftingio/pagination.py +84 -0
- siftingio-0.1.0/src/siftingio/py.typed +0 -0
- siftingio-0.1.0/src/siftingio/resources/__init__.py +1 -0
- siftingio-0.1.0/src/siftingio/resources/_base.py +44 -0
- siftingio-0.1.0/src/siftingio/resources/crypto.py +38 -0
- siftingio-0.1.0/src/siftingio/resources/dex.py +21 -0
- siftingio-0.1.0/src/siftingio/resources/economic_calendar.py +39 -0
- siftingio-0.1.0/src/siftingio/resources/filers.py +25 -0
- siftingio-0.1.0/src/siftingio/resources/forex.py +33 -0
- siftingio-0.1.0/src/siftingio/resources/last.py +42 -0
- siftingio-0.1.0/src/siftingio/resources/markets.py +70 -0
- siftingio-0.1.0/src/siftingio/resources/stocks.py +275 -0
- siftingio-0.1.0/src/siftingio/types.py +528 -0
- siftingio-0.1.0/src/siftingio/ws/__init__.py +5 -0
- siftingio-0.1.0/src/siftingio/ws/client.py +308 -0
- siftingio-0.1.0/src/siftingio/ws/types.py +45 -0
- siftingio-0.1.0/tests/test_client.py +169 -0
- siftingio-0.1.0/tests/test_ws.py +84 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `siftingio` are documented here. Format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com); the project adheres to SemVer.
|
|
5
|
+
|
|
6
|
+
## [0.1.0] — Unreleased
|
|
7
|
+
|
|
8
|
+
Initial release.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- `SiftingClient` (sync) and `AsyncSiftingClient` (async) covering the full data
|
|
12
|
+
plane: `last`, `stocks`, `filers`, `markets`, `forex`, `crypto`, `dex`,
|
|
13
|
+
`economic_calendar`.
|
|
14
|
+
- Live WebSocket clients: `AsyncSiftingSocket` (asyncio) and a thread-backed
|
|
15
|
+
sync `SiftingSocket`, both with auto-reconnect and subscription replay.
|
|
16
|
+
- Cursor auto-pagination: `auto_paginate` / `collect_all` and async
|
|
17
|
+
`aauto_paginate` / `acollect_all`.
|
|
18
|
+
- Automatic retries (429/5xx with `Retry-After`), gzip negotiation, per-request
|
|
19
|
+
timeouts, and typed errors (`SiftingAPIError`, `SiftingConnectionError`).
|
|
20
|
+
- Full type hints (`py.typed`); responses typed via `TypedDict`.
|
|
21
|
+
- Python 3.9+; depends only on `httpx` and `websockets`.
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# CLAUDE.md — `siftingio` (Python)
|
|
2
|
+
|
|
3
|
+
Guidance for maintaining and extending the SiftingIO Python SDK. It wraps the
|
|
4
|
+
**data plane only** — the API-key-authenticated `/v1/*` endpoints from
|
|
5
|
+
`SiftingIO_API_V1/router/router.go`. It does **not** wrap the `/ops/v1/*`
|
|
6
|
+
control plane (auth, billing, account); that is the SPA's surface.
|
|
7
|
+
|
|
8
|
+
The package mirrors the TypeScript SDK (`../typescript`) endpoint-for-endpoint,
|
|
9
|
+
so keep the two in sync when the API changes.
|
|
10
|
+
|
|
11
|
+
## Layout
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
src/siftingio/
|
|
15
|
+
__init__.py — public surface + __version__ (hatch reads this)
|
|
16
|
+
client.py — SiftingClient (sync) + AsyncSiftingClient (async)
|
|
17
|
+
_transport.py — _SyncTransport / _AsyncTransport: auth, gzip, retries, errors
|
|
18
|
+
errors.py — SiftingError / SiftingAPIError / SiftingConnectionError
|
|
19
|
+
types.py — TypedDict response shapes + Literal unions
|
|
20
|
+
pagination.py — auto_paginate / collect_all (+ async aauto_paginate / acollect_all)
|
|
21
|
+
resources/
|
|
22
|
+
_base.py — Req namedtuple, seg(), _SyncResource / _AsyncResource bases
|
|
23
|
+
*.py — one module per namespace; see "sync/async pattern" below
|
|
24
|
+
ws/
|
|
25
|
+
client.py — AsyncSiftingSocket + thread-backed sync SiftingSocket
|
|
26
|
+
types.py — WebSocket protocol TypedDicts
|
|
27
|
+
tests/ — pytest (httpx.MockTransport for REST, a local ws server for WS)
|
|
28
|
+
examples/ — runnable rest.py / websocket.py
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## The sync/async pattern (important)
|
|
32
|
+
|
|
33
|
+
Each resource module has **one request-builder per endpoint** (`_profile`,
|
|
34
|
+
`_filings`, …) that returns a `Req(path, params)` — this is the single source of
|
|
35
|
+
truth for routing. Then two thin classes, `XResource` (sync) and
|
|
36
|
+
`AsyncXResource` (async), each expose one-line methods that call `self._get` /
|
|
37
|
+
`self._aget`. When you add an endpoint you write: one builder + two one-liners +
|
|
38
|
+
a `TypedDict`. The actual logic lives only in the builder.
|
|
39
|
+
|
|
40
|
+
Python keyword collisions (`from`) use a trailing-underscore kwarg (`from_`) on
|
|
41
|
+
the public method; the builder maps it to the real query key (`"from"`).
|
|
42
|
+
|
|
43
|
+
## Commands
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
python -m venv .venv && .venv/bin/pip install -e ".[dev]"
|
|
47
|
+
.venv/bin/pytest -q # tests (REST + WebSocket)
|
|
48
|
+
.venv/bin/mypy src/siftingio # strict type check
|
|
49
|
+
.venv/bin/ruff check src # lint
|
|
50
|
+
.venv/bin/python -m build --wheel # build distribution
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Adding or changing an endpoint
|
|
54
|
+
|
|
55
|
+
The Go API is the source of truth — read `router.go` for the route and
|
|
56
|
+
`model/fundamentals.go` (or the relevant `service/*`) for the response struct.
|
|
57
|
+
|
|
58
|
+
1. Add a `_req` builder in the matching `resources/*.py`, encoding every dynamic
|
|
59
|
+
segment with `seg(...)` and listing query params in the params dict.
|
|
60
|
+
2. Add a `TypedDict` in `types.py` mirroring the Go struct field-for-field
|
|
61
|
+
(JSON casing, `total=False`).
|
|
62
|
+
3. Add the sync method to `XResource` and the async method to `AsyncXResource`.
|
|
63
|
+
4. Re-export new public types from `__init__.py` if user-facing.
|
|
64
|
+
5. Add a test in `tests/` using `httpx.MockTransport`.
|
|
65
|
+
6. Run `pytest`, `mypy`, `ruff`. Mirror the change in the TypeScript SDK.
|
|
66
|
+
|
|
67
|
+
### Gzip-required endpoints
|
|
68
|
+
|
|
69
|
+
`stocks.screener`, `stocks.financials`, `stocks.financial_concept`,
|
|
70
|
+
`stocks.bars`, `forex.bars`, `crypto.bars` return 406 without
|
|
71
|
+
`Accept-Encoding: gzip`. The transport sends it on every request and httpx
|
|
72
|
+
decompresses transparently, so no per-method handling is needed.
|
|
73
|
+
|
|
74
|
+
## Conventions
|
|
75
|
+
|
|
76
|
+
- Runtime deps are only `httpx` + `websockets`. Don't add more without cause.
|
|
77
|
+
- `from __future__ import annotations` at the top of every module — this is what
|
|
78
|
+
lets us use `list[...]` / `X | None` while still supporting Python 3.9.
|
|
79
|
+
- Responses are returned as raw dicts (typed via `TypedDict`); we don't validate
|
|
80
|
+
or construct models at runtime. `mypy` runs with `warn_return_any = false`
|
|
81
|
+
because of this.
|
|
82
|
+
- Resources never import httpx — only the transport does.
|
|
83
|
+
- Semver, mirrored with the TS package. Keep `CHANGELOG.md` and `__version__`
|
|
84
|
+
in step.
|
|
85
|
+
|
|
86
|
+
## Releasing
|
|
87
|
+
|
|
88
|
+
`pytest && mypy src/siftingio && ruff check src` → bump `__version__` →
|
|
89
|
+
update `CHANGELOG.md` → `python -m build` → `twine upload dist/*`.
|
siftingio-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SiftingIO
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
siftingio-0.1.0/Makefile
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# Makefile for the siftingio Python SDK.
|
|
2
|
+
#
|
|
3
|
+
# Publishing reads the PyPI token from the environment — never hardcode it here.
|
|
4
|
+
# Set it for the session (don't commit it anywhere):
|
|
5
|
+
#
|
|
6
|
+
# export TWINE_PASSWORD='pypi-...' # the token value
|
|
7
|
+
# make publish-test # upload to TestPyPI first
|
|
8
|
+
# make publish # upload to real PyPI
|
|
9
|
+
#
|
|
10
|
+
# TWINE_USERNAME defaults to __token__ (correct for API-token auth).
|
|
11
|
+
|
|
12
|
+
PY := .venv/bin/python
|
|
13
|
+
PIP := .venv/bin/pip
|
|
14
|
+
VENV := .venv
|
|
15
|
+
|
|
16
|
+
export TWINE_USERNAME ?= __token__
|
|
17
|
+
|
|
18
|
+
.PHONY: help venv install test lint typecheck check build dist-check \
|
|
19
|
+
publish-test publish clean
|
|
20
|
+
|
|
21
|
+
help:
|
|
22
|
+
@echo "Targets:"
|
|
23
|
+
@echo " venv Create the virtualenv (.venv)"
|
|
24
|
+
@echo " install Install the package + dev tools (editable)"
|
|
25
|
+
@echo " test Run the test suite"
|
|
26
|
+
@echo " lint Run ruff"
|
|
27
|
+
@echo " typecheck Run mypy (strict)"
|
|
28
|
+
@echo " check lint + typecheck + test"
|
|
29
|
+
@echo " build Build sdist + wheel into dist/"
|
|
30
|
+
@echo " dist-check Validate built artifacts with twine check"
|
|
31
|
+
@echo " publish-test Upload to TestPyPI (needs TWINE_PASSWORD)"
|
|
32
|
+
@echo " publish Upload to PyPI (needs TWINE_PASSWORD)"
|
|
33
|
+
@echo " clean Remove build artifacts and caches"
|
|
34
|
+
|
|
35
|
+
$(VENV):
|
|
36
|
+
python3 -m venv $(VENV)
|
|
37
|
+
|
|
38
|
+
venv: $(VENV)
|
|
39
|
+
|
|
40
|
+
install: venv
|
|
41
|
+
$(PIP) install -e ".[dev]"
|
|
42
|
+
|
|
43
|
+
test:
|
|
44
|
+
$(PY) -m pytest -q
|
|
45
|
+
|
|
46
|
+
lint:
|
|
47
|
+
$(PY) -m ruff check src
|
|
48
|
+
|
|
49
|
+
typecheck:
|
|
50
|
+
$(PY) -m mypy src/siftingio
|
|
51
|
+
|
|
52
|
+
check: lint typecheck test
|
|
53
|
+
|
|
54
|
+
build: clean
|
|
55
|
+
$(PY) -m build
|
|
56
|
+
|
|
57
|
+
dist-check:
|
|
58
|
+
$(PY) -m twine check dist/*
|
|
59
|
+
|
|
60
|
+
# Gated on a green build + metadata check so a bad artifact never ships.
|
|
61
|
+
publish-test: check build dist-check
|
|
62
|
+
@test -n "$$TWINE_PASSWORD" || { echo "ERROR: set TWINE_PASSWORD to your TestPyPI token"; exit 1; }
|
|
63
|
+
$(PY) -m twine upload --repository testpypi dist/*
|
|
64
|
+
|
|
65
|
+
publish: check build dist-check
|
|
66
|
+
@test -n "$$TWINE_PASSWORD" || { echo "ERROR: set TWINE_PASSWORD to your PyPI token"; exit 1; }
|
|
67
|
+
$(PY) -m twine upload dist/*
|
|
68
|
+
|
|
69
|
+
clean:
|
|
70
|
+
rm -rf dist build *.egg-info src/*.egg-info
|
|
71
|
+
rm -rf .pytest_cache .mypy_cache .ruff_cache
|
|
72
|
+
find . -type d -name __pycache__ -prune -exec rm -rf {} +
|
siftingio-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: siftingio
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the SiftingIO market data API (sync + async REST, plus WebSocket).
|
|
5
|
+
Project-URL: Homepage, https://sifting.io
|
|
6
|
+
Project-URL: Documentation, https://sifting.io/docs
|
|
7
|
+
Project-URL: Source, https://github.com/siftingio/sdk-python
|
|
8
|
+
Author: SiftingIO
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api,crypto,edgar,forex,fundamentals,market-data,sdk,sec,sifting,siftingio,stocks,websocket,xbrl
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.9
|
|
23
|
+
Requires-Dist: httpx>=0.27
|
|
24
|
+
Requires-Dist: websockets>=12
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build>=1.2; extra == 'dev'
|
|
27
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
31
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# siftingio
|
|
35
|
+
|
|
36
|
+
Official Python SDK for the [SiftingIO](https://sifting.io) market data API — **sync and async** REST clients plus a live WebSocket, fully type-hinted.
|
|
37
|
+
|
|
38
|
+
- **Sync *and* async.** `SiftingClient` for scripts, notebooks, and pandas; `AsyncSiftingClient` for asyncio services. Same method names, same shapes.
|
|
39
|
+
- **Typed.** Every endpoint, parameter, and response is annotated (`py.typed`); responses are plain dicts with `TypedDict` shapes for editor autocomplete.
|
|
40
|
+
- **Resource-mapped.** Methods mirror the [API docs](https://sifting.io/docs) 1:1.
|
|
41
|
+
- **Batteries included.** Auto-retry on 429/5xx, gzip negotiation, cursor auto-pagination, and an auto-reconnecting WebSocket client.
|
|
42
|
+
|
|
43
|
+
## Install
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install siftingio
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Requires Python 3.9+. Depends only on `httpx` and `websockets`.
|
|
50
|
+
|
|
51
|
+
## Quick start (sync)
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from siftingio import SiftingClient
|
|
55
|
+
|
|
56
|
+
client = SiftingClient(api_key="sft_...") # or env-driven; see below
|
|
57
|
+
|
|
58
|
+
# Live price
|
|
59
|
+
trade = client.last.trade("crypto", "BTCUSDT")
|
|
60
|
+
print(trade["p"], trade["t"])
|
|
61
|
+
|
|
62
|
+
# Company fundamentals
|
|
63
|
+
profile = client.stocks.profile("AAPL")
|
|
64
|
+
ratios = client.stocks.ratios("AAPL")
|
|
65
|
+
|
|
66
|
+
# Historical bars (gzip handled for you)
|
|
67
|
+
bars = client.crypto.bars("BTCUSD", start="2024-01-01", interval="1h")
|
|
68
|
+
print(len(bars["data"]), "bars")
|
|
69
|
+
|
|
70
|
+
client.close() # or use `with SiftingClient(...) as client:`
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Quick start (async)
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import asyncio
|
|
77
|
+
from siftingio import AsyncSiftingClient
|
|
78
|
+
|
|
79
|
+
async def main():
|
|
80
|
+
async with AsyncSiftingClient(api_key="sft_...") as client:
|
|
81
|
+
quote = await client.last.quote("crypto", "ETHUSDT")
|
|
82
|
+
print(quote["b"], quote["a"])
|
|
83
|
+
|
|
84
|
+
asyncio.run(main())
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Authentication
|
|
88
|
+
|
|
89
|
+
Get an API key from your [SiftingIO dashboard](https://sifting.io). It's sent as the `X-API-Key` header. You can also supply it dynamically (e.g. from a secrets manager or a rotating token):
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
client = SiftingClient(get_api_key=lambda: read_secret("SIFTING_API_KEY"))
|
|
93
|
+
|
|
94
|
+
# Async: the hook may be sync or async
|
|
95
|
+
async_client = AsyncSiftingClient(get_api_key=fetch_token_async)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Configuration
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
SiftingClient(
|
|
102
|
+
api_key="sft_...", # X-API-Key header
|
|
103
|
+
get_api_key=callable, # dynamic alternative to api_key
|
|
104
|
+
base_url="https://api.sifting.io", # override for proxies/staging
|
|
105
|
+
ws_url="wss://stream.sifting.io/ws/v1", # WebSocket endpoint
|
|
106
|
+
timeout=30.0, # per-request timeout (seconds)
|
|
107
|
+
max_retries=2, # automatic retries for 429 / 5xx
|
|
108
|
+
headers={"X-Trace": "…"},# extra headers on every request
|
|
109
|
+
http_client=httpx.Client(...), # bring your own httpx client
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`AsyncSiftingClient` takes the same arguments (with `httpx.AsyncClient`).
|
|
114
|
+
|
|
115
|
+
## Resources
|
|
116
|
+
|
|
117
|
+
| Namespace | Endpoints | Highlights |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `client.last` | `/v1/last/*` | `trade`, `quote`, `tvl` — live snapshots |
|
|
120
|
+
| `client.stocks` | `/v1/fnd/stocks/*`, `/v1/hist/stocks/*` | `search`, `profile`, `filings`, `financials`, `ratios`, `insiders`, `events`, `screener`, `bars`, … |
|
|
121
|
+
| `client.filers` | `/v1/fnd/filers/*` | `holdings` — 13F positions |
|
|
122
|
+
| `client.markets` | `/v1/fnd/markets/*` | `list`, `status`, `hours`, `calendar` |
|
|
123
|
+
| `client.forex` | `/v1/hist/forex/*` | `bars` |
|
|
124
|
+
| `client.crypto` | `/v1/hist/crypto/*` | `bars` |
|
|
125
|
+
| `client.dex` | `/v1/fnd/dex/*` | `wallet` portfolios |
|
|
126
|
+
| `client.economic_calendar` | `/v1/fnd/economic-calendar` | `list` |
|
|
127
|
+
|
|
128
|
+
> Python keyword params that collide with reserved words use a trailing underscore: pass `from_=...` (sent to the API as `from`).
|
|
129
|
+
|
|
130
|
+
## Pagination
|
|
131
|
+
|
|
132
|
+
List endpoints return `{"data": [...], "meta": {...}}` with an opaque `meta["next_cursor"]`. Stream every page with `auto_paginate` (sync) or `aauto_paginate` (async):
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from siftingio import auto_paginate, collect_all
|
|
136
|
+
|
|
137
|
+
for filing in auto_paginate(lambda cursor: client.stocks.filings("AAPL", cursor=cursor, form="10-K")):
|
|
138
|
+
print(filing["accession"], filing["filed_at"])
|
|
139
|
+
|
|
140
|
+
insiders = collect_all(lambda cursor: client.stocks.insiders("TSLA", cursor=cursor), max_items=100)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from siftingio import aauto_paginate
|
|
145
|
+
|
|
146
|
+
async for filing in aauto_paginate(lambda cursor: client.stocks.filings("AAPL", cursor=cursor)):
|
|
147
|
+
...
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Live WebSocket
|
|
151
|
+
|
|
152
|
+
**Async:**
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
async with client.ws() as socket: # client = AsyncSiftingClient(...)
|
|
156
|
+
socket.on("tick", lambda t: print(t["s"], t.get("p")))
|
|
157
|
+
socket.on("error", lambda e: print("server error:", e["code"], e["message"]))
|
|
158
|
+
await socket.subscribe("cex", ["BTCUSDT", "ETHUSDT"]) # products: cex|dex|fx|us|tvl
|
|
159
|
+
async for frame in socket: # or rely purely on handlers
|
|
160
|
+
...
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**Sync:**
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
socket = client.ws() # client = SiftingClient(...)
|
|
167
|
+
socket.on("tick", lambda t: print(t["s"], t.get("p")))
|
|
168
|
+
socket.connect()
|
|
169
|
+
socket.subscribe("cex", ["BTCUSDT"])
|
|
170
|
+
for frame in socket.stream():
|
|
171
|
+
...
|
|
172
|
+
socket.close()
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Subscriptions are tracked and **replayed automatically on reconnect**, so you subscribe once and keep receiving data across drops. In the sync client, handlers run on a background thread.
|
|
176
|
+
|
|
177
|
+
## Error handling
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from siftingio import SiftingAPIError, SiftingConnectionError
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
client.stocks.profile("NOPE")
|
|
184
|
+
except SiftingAPIError as err:
|
|
185
|
+
err.status # 404
|
|
186
|
+
err.code # "unknown_ticker"
|
|
187
|
+
err.retry_after # seconds, on 429
|
|
188
|
+
err.request_id # X-Request-Id — quote this in support tickets
|
|
189
|
+
err.body # full parsed error body
|
|
190
|
+
except SiftingConnectionError as err:
|
|
191
|
+
err.timeout # True if it was a client-side timeout
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The client automatically retries `429` and `5xx` up to `max_retries`, honoring `Retry-After`.
|
|
195
|
+
|
|
196
|
+
## License
|
|
197
|
+
|
|
198
|
+
MIT
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# siftingio
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [SiftingIO](https://sifting.io) market data API — **sync and async** REST clients plus a live WebSocket, fully type-hinted.
|
|
4
|
+
|
|
5
|
+
- **Sync *and* async.** `SiftingClient` for scripts, notebooks, and pandas; `AsyncSiftingClient` for asyncio services. Same method names, same shapes.
|
|
6
|
+
- **Typed.** Every endpoint, parameter, and response is annotated (`py.typed`); responses are plain dicts with `TypedDict` shapes for editor autocomplete.
|
|
7
|
+
- **Resource-mapped.** Methods mirror the [API docs](https://sifting.io/docs) 1:1.
|
|
8
|
+
- **Batteries included.** Auto-retry on 429/5xx, gzip negotiation, cursor auto-pagination, and an auto-reconnecting WebSocket client.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install siftingio
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Requires Python 3.9+. Depends only on `httpx` and `websockets`.
|
|
17
|
+
|
|
18
|
+
## Quick start (sync)
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from siftingio import SiftingClient
|
|
22
|
+
|
|
23
|
+
client = SiftingClient(api_key="sft_...") # or env-driven; see below
|
|
24
|
+
|
|
25
|
+
# Live price
|
|
26
|
+
trade = client.last.trade("crypto", "BTCUSDT")
|
|
27
|
+
print(trade["p"], trade["t"])
|
|
28
|
+
|
|
29
|
+
# Company fundamentals
|
|
30
|
+
profile = client.stocks.profile("AAPL")
|
|
31
|
+
ratios = client.stocks.ratios("AAPL")
|
|
32
|
+
|
|
33
|
+
# Historical bars (gzip handled for you)
|
|
34
|
+
bars = client.crypto.bars("BTCUSD", start="2024-01-01", interval="1h")
|
|
35
|
+
print(len(bars["data"]), "bars")
|
|
36
|
+
|
|
37
|
+
client.close() # or use `with SiftingClient(...) as client:`
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick start (async)
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import asyncio
|
|
44
|
+
from siftingio import AsyncSiftingClient
|
|
45
|
+
|
|
46
|
+
async def main():
|
|
47
|
+
async with AsyncSiftingClient(api_key="sft_...") as client:
|
|
48
|
+
quote = await client.last.quote("crypto", "ETHUSDT")
|
|
49
|
+
print(quote["b"], quote["a"])
|
|
50
|
+
|
|
51
|
+
asyncio.run(main())
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Authentication
|
|
55
|
+
|
|
56
|
+
Get an API key from your [SiftingIO dashboard](https://sifting.io). It's sent as the `X-API-Key` header. You can also supply it dynamically (e.g. from a secrets manager or a rotating token):
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
client = SiftingClient(get_api_key=lambda: read_secret("SIFTING_API_KEY"))
|
|
60
|
+
|
|
61
|
+
# Async: the hook may be sync or async
|
|
62
|
+
async_client = AsyncSiftingClient(get_api_key=fetch_token_async)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Configuration
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
SiftingClient(
|
|
69
|
+
api_key="sft_...", # X-API-Key header
|
|
70
|
+
get_api_key=callable, # dynamic alternative to api_key
|
|
71
|
+
base_url="https://api.sifting.io", # override for proxies/staging
|
|
72
|
+
ws_url="wss://stream.sifting.io/ws/v1", # WebSocket endpoint
|
|
73
|
+
timeout=30.0, # per-request timeout (seconds)
|
|
74
|
+
max_retries=2, # automatic retries for 429 / 5xx
|
|
75
|
+
headers={"X-Trace": "…"},# extra headers on every request
|
|
76
|
+
http_client=httpx.Client(...), # bring your own httpx client
|
|
77
|
+
)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`AsyncSiftingClient` takes the same arguments (with `httpx.AsyncClient`).
|
|
81
|
+
|
|
82
|
+
## Resources
|
|
83
|
+
|
|
84
|
+
| Namespace | Endpoints | Highlights |
|
|
85
|
+
|---|---|---|
|
|
86
|
+
| `client.last` | `/v1/last/*` | `trade`, `quote`, `tvl` — live snapshots |
|
|
87
|
+
| `client.stocks` | `/v1/fnd/stocks/*`, `/v1/hist/stocks/*` | `search`, `profile`, `filings`, `financials`, `ratios`, `insiders`, `events`, `screener`, `bars`, … |
|
|
88
|
+
| `client.filers` | `/v1/fnd/filers/*` | `holdings` — 13F positions |
|
|
89
|
+
| `client.markets` | `/v1/fnd/markets/*` | `list`, `status`, `hours`, `calendar` |
|
|
90
|
+
| `client.forex` | `/v1/hist/forex/*` | `bars` |
|
|
91
|
+
| `client.crypto` | `/v1/hist/crypto/*` | `bars` |
|
|
92
|
+
| `client.dex` | `/v1/fnd/dex/*` | `wallet` portfolios |
|
|
93
|
+
| `client.economic_calendar` | `/v1/fnd/economic-calendar` | `list` |
|
|
94
|
+
|
|
95
|
+
> Python keyword params that collide with reserved words use a trailing underscore: pass `from_=...` (sent to the API as `from`).
|
|
96
|
+
|
|
97
|
+
## Pagination
|
|
98
|
+
|
|
99
|
+
List endpoints return `{"data": [...], "meta": {...}}` with an opaque `meta["next_cursor"]`. Stream every page with `auto_paginate` (sync) or `aauto_paginate` (async):
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from siftingio import auto_paginate, collect_all
|
|
103
|
+
|
|
104
|
+
for filing in auto_paginate(lambda cursor: client.stocks.filings("AAPL", cursor=cursor, form="10-K")):
|
|
105
|
+
print(filing["accession"], filing["filed_at"])
|
|
106
|
+
|
|
107
|
+
insiders = collect_all(lambda cursor: client.stocks.insiders("TSLA", cursor=cursor), max_items=100)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from siftingio import aauto_paginate
|
|
112
|
+
|
|
113
|
+
async for filing in aauto_paginate(lambda cursor: client.stocks.filings("AAPL", cursor=cursor)):
|
|
114
|
+
...
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Live WebSocket
|
|
118
|
+
|
|
119
|
+
**Async:**
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
async with client.ws() as socket: # client = AsyncSiftingClient(...)
|
|
123
|
+
socket.on("tick", lambda t: print(t["s"], t.get("p")))
|
|
124
|
+
socket.on("error", lambda e: print("server error:", e["code"], e["message"]))
|
|
125
|
+
await socket.subscribe("cex", ["BTCUSDT", "ETHUSDT"]) # products: cex|dex|fx|us|tvl
|
|
126
|
+
async for frame in socket: # or rely purely on handlers
|
|
127
|
+
...
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Sync:**
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
socket = client.ws() # client = SiftingClient(...)
|
|
134
|
+
socket.on("tick", lambda t: print(t["s"], t.get("p")))
|
|
135
|
+
socket.connect()
|
|
136
|
+
socket.subscribe("cex", ["BTCUSDT"])
|
|
137
|
+
for frame in socket.stream():
|
|
138
|
+
...
|
|
139
|
+
socket.close()
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Subscriptions are tracked and **replayed automatically on reconnect**, so you subscribe once and keep receiving data across drops. In the sync client, handlers run on a background thread.
|
|
143
|
+
|
|
144
|
+
## Error handling
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from siftingio import SiftingAPIError, SiftingConnectionError
|
|
148
|
+
|
|
149
|
+
try:
|
|
150
|
+
client.stocks.profile("NOPE")
|
|
151
|
+
except SiftingAPIError as err:
|
|
152
|
+
err.status # 404
|
|
153
|
+
err.code # "unknown_ticker"
|
|
154
|
+
err.retry_after # seconds, on 429
|
|
155
|
+
err.request_id # X-Request-Id — quote this in support tickets
|
|
156
|
+
err.body # full parsed error body
|
|
157
|
+
except SiftingConnectionError as err:
|
|
158
|
+
err.timeout # True if it was a client-side timeout
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The client automatically retries `429` and `5xx` up to `max_retries`, honoring `Retry-After`.
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""REST quick tour (sync). Run with: SIFTING_API_KEY=sft_… python examples/rest.py"""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from siftingio import SiftingAPIError, SiftingClient, auto_paginate
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def main() -> None:
|
|
9
|
+
with SiftingClient(api_key=os.environ.get("SIFTING_API_KEY")) as client:
|
|
10
|
+
# 1. Live snapshot
|
|
11
|
+
trade = client.last.trade("crypto", "BTCUSDT")
|
|
12
|
+
print("BTC last trade:", trade["p"], "@", trade["t"])
|
|
13
|
+
|
|
14
|
+
# 2. Fundamentals
|
|
15
|
+
profile = client.stocks.profile("AAPL")
|
|
16
|
+
print(f"{profile['name']} ({profile['ticker']}) — {profile.get('sic_description')}")
|
|
17
|
+
|
|
18
|
+
ratios = client.stocks.ratios("AAPL")
|
|
19
|
+
latest = ratios.get("latest") or {}
|
|
20
|
+
print("Latest net margin:", latest.get("net_margin"))
|
|
21
|
+
|
|
22
|
+
# 3. Historical bars (gzip negotiated automatically)
|
|
23
|
+
bars = client.crypto.bars("ETHUSD", start="2024-01-01", end="2024-01-02", interval="1h")
|
|
24
|
+
print(f"Got {len(bars['data'])} ETH bars")
|
|
25
|
+
|
|
26
|
+
# 4. Auto-paginated 10-K filings
|
|
27
|
+
count = 0
|
|
28
|
+
for filing in auto_paginate(
|
|
29
|
+
lambda cursor: client.stocks.filings("AAPL", cursor=cursor, form="10-K")
|
|
30
|
+
):
|
|
31
|
+
count += 1
|
|
32
|
+
if count <= 3:
|
|
33
|
+
print("10-K:", filing["filed_at"], filing["accession"])
|
|
34
|
+
print(f"Total 10-Ks: {count}")
|
|
35
|
+
|
|
36
|
+
# 5. Markets + economic calendar
|
|
37
|
+
status = client.markets.status("us_equities")
|
|
38
|
+
print("US equities open?", status["data"].get("is_open"))
|
|
39
|
+
|
|
40
|
+
cal = client.economic_calendar.list(impact="high", limit=5)
|
|
41
|
+
print(f"{cal['count']} high-impact events upcoming")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
if __name__ == "__main__":
|
|
45
|
+
try:
|
|
46
|
+
main()
|
|
47
|
+
except SiftingAPIError as err:
|
|
48
|
+
raise SystemExit(f"API error {err.status} ({err.code}): {err.message}") from err
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Live WebSocket tour. Run with: SIFTING_API_KEY=sft_… python examples/websocket.py
|
|
2
|
+
|
|
3
|
+
Shows both the async-native client and (commented) the blocking client.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import os
|
|
8
|
+
|
|
9
|
+
from siftingio import AsyncSiftingClient
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def main() -> None:
|
|
13
|
+
client = AsyncSiftingClient(api_key=os.environ.get("SIFTING_API_KEY"))
|
|
14
|
+
socket = client.ws()
|
|
15
|
+
|
|
16
|
+
socket.on("open", lambda _: print("connected"))
|
|
17
|
+
socket.on("reconnect", lambda info: print("reconnecting, attempt", info["attempt"]))
|
|
18
|
+
socket.on("error", lambda e: print("server error:", e.get("code"), e.get("message")))
|
|
19
|
+
socket.on("tick", lambda t: print(f"[{t.get('class', 'tick')}] {t['s']} {t.get('p') or t.get('b')}"))
|
|
20
|
+
socket.on("tvl", lambda v: print(f"[tvl] {v['s']} ${v['usd']}"))
|
|
21
|
+
|
|
22
|
+
await socket.connect()
|
|
23
|
+
await socket.subscribe("cex", ["BTCUSDT", "ETHUSDT"])
|
|
24
|
+
await socket.subscribe("tvl", ["eth:WETH-USDC"])
|
|
25
|
+
|
|
26
|
+
# Stream for 30 seconds, then close.
|
|
27
|
+
await asyncio.sleep(30)
|
|
28
|
+
await socket.close()
|
|
29
|
+
await client.aclose()
|
|
30
|
+
print("done")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# Blocking equivalent:
|
|
34
|
+
#
|
|
35
|
+
# from siftingio import SiftingClient
|
|
36
|
+
# client = SiftingClient(api_key=os.environ["SIFTING_API_KEY"])
|
|
37
|
+
# socket = client.ws()
|
|
38
|
+
# socket.on("tick", lambda t: print(t["s"], t.get("p")))
|
|
39
|
+
# socket.connect()
|
|
40
|
+
# socket.subscribe("cex", ["BTCUSDT"])
|
|
41
|
+
# for frame in socket.stream():
|
|
42
|
+
# ...
|
|
43
|
+
# socket.close()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
asyncio.run(main())
|