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.
Files changed (32) hide show
  1. siftingio-0.1.0/.gitignore +10 -0
  2. siftingio-0.1.0/CHANGELOG.md +21 -0
  3. siftingio-0.1.0/CLAUDE.md +89 -0
  4. siftingio-0.1.0/LICENSE +21 -0
  5. siftingio-0.1.0/Makefile +72 -0
  6. siftingio-0.1.0/PKG-INFO +198 -0
  7. siftingio-0.1.0/README.md +165 -0
  8. siftingio-0.1.0/examples/rest.py +48 -0
  9. siftingio-0.1.0/examples/websocket.py +47 -0
  10. siftingio-0.1.0/pyproject.toml +75 -0
  11. siftingio-0.1.0/src/siftingio/__init__.py +30 -0
  12. siftingio-0.1.0/src/siftingio/_transport.py +249 -0
  13. siftingio-0.1.0/src/siftingio/client.py +143 -0
  14. siftingio-0.1.0/src/siftingio/errors.py +52 -0
  15. siftingio-0.1.0/src/siftingio/pagination.py +84 -0
  16. siftingio-0.1.0/src/siftingio/py.typed +0 -0
  17. siftingio-0.1.0/src/siftingio/resources/__init__.py +1 -0
  18. siftingio-0.1.0/src/siftingio/resources/_base.py +44 -0
  19. siftingio-0.1.0/src/siftingio/resources/crypto.py +38 -0
  20. siftingio-0.1.0/src/siftingio/resources/dex.py +21 -0
  21. siftingio-0.1.0/src/siftingio/resources/economic_calendar.py +39 -0
  22. siftingio-0.1.0/src/siftingio/resources/filers.py +25 -0
  23. siftingio-0.1.0/src/siftingio/resources/forex.py +33 -0
  24. siftingio-0.1.0/src/siftingio/resources/last.py +42 -0
  25. siftingio-0.1.0/src/siftingio/resources/markets.py +70 -0
  26. siftingio-0.1.0/src/siftingio/resources/stocks.py +275 -0
  27. siftingio-0.1.0/src/siftingio/types.py +528 -0
  28. siftingio-0.1.0/src/siftingio/ws/__init__.py +5 -0
  29. siftingio-0.1.0/src/siftingio/ws/client.py +308 -0
  30. siftingio-0.1.0/src/siftingio/ws/types.py +45 -0
  31. siftingio-0.1.0/tests/test_client.py +169 -0
  32. siftingio-0.1.0/tests/test_ws.py +84 -0
@@ -0,0 +1,10 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .DS_Store
@@ -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/*`.
@@ -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.
@@ -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 {} +
@@ -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())