sahiapi 0.1.0__tar.gz → 0.3.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.
- sahiapi-0.3.0/CHANGELOG.md +82 -0
- sahiapi-0.3.0/LICENSE +21 -0
- sahiapi-0.3.0/PKG-INFO +111 -0
- sahiapi-0.3.0/README.md +79 -0
- {sahiapi-0.1.0 → sahiapi-0.3.0}/pyproject.toml +6 -6
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/__init__.py +4 -0
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/auth.py +30 -13
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/client.py +13 -7
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/env.py +7 -1
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/exceptions.py +88 -1
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/feed.py +7 -7
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/funds.py +5 -6
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/http.py +10 -0
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/instruments.py +4 -13
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/marketdata.py +15 -16
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/proto/codec.py +44 -10
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/ws_client.py +7 -5
- sahiapi-0.1.0/CHANGELOG.md +0 -30
- sahiapi-0.1.0/LICENSE +0 -7
- sahiapi-0.1.0/PKG-INFO +0 -97
- sahiapi-0.1.0/README.md +0 -57
- {sahiapi-0.1.0 → sahiapi-0.3.0}/.gitignore +0 -0
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/py.typed +0 -0
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/__init__.py +0 -0
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/account.py +0 -0
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/base.py +0 -0
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/constants.py +0 -0
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/orders.py +0 -0
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/portfolio.py +0 -0
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/proto/__init__.py +0 -0
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/proto/marketdata.proto +0 -0
- {sahiapi-0.1.0 → sahiapi-0.3.0}/src/sahiapi/sahi/proto/marketdata_pb2.py +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here. The format follows
|
|
4
|
+
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to
|
|
5
|
+
follow [Semantic Versioning](https://semver.org/) once it reaches `1.0.0`.
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.3.0] - 2026-10-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- TOTP keys: `SahiAPI.from_credentials` and `get_access_token` take `totp`, the
|
|
13
|
+
key's current six-digit authenticator code, which doubles as the day's
|
|
14
|
+
approval. A wrong code raises `SahiInvalidTotpException`, carrying the
|
|
15
|
+
server's `server_time` for clock comparison.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- The `SahiKeyNotApprovedException` hint also covers an account that does not
|
|
19
|
+
have API keys enabled yet. A TOTP key gets this exception only for that reason.
|
|
20
|
+
|
|
21
|
+
## [0.2.0] - 2026-10-22
|
|
22
|
+
|
|
23
|
+
First public release. Includes everything listed under 0.1.0.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- `SahiMaintenanceException` (a `SahiServerException`) for `503 UNDER_MAINTENANCE`,
|
|
27
|
+
the margin endpoints' daily 04:45–07:00 IST window, with a hint that says so
|
|
28
|
+
instead of "retry after a short wait".
|
|
29
|
+
|
|
30
|
+
### Changed
|
|
31
|
+
- `SahiFeed` cache reads are `last_ltp`, `last_quote` and `last_depth` (were
|
|
32
|
+
`get_ltp`, `get_quote`, `get_depth`), so they no longer share a name with the
|
|
33
|
+
REST calls `SahiAPI.get_ltp` / `get_quotes` that have a different signature.
|
|
34
|
+
- `get_ltp` / `get_quotes` no longer cap the id list locally. The SDK said 500
|
|
35
|
+
while the API allows 100, which is the trouble with a limit baked into a
|
|
36
|
+
release: the server's `400 VALIDATION_FAILED` names the limit, and the
|
|
37
|
+
docstrings state it.
|
|
38
|
+
- The scrip master downloads from the published URL by default
|
|
39
|
+
(`https://assets.sahi.com/external/instruments.csv`); `scrip_master_url=` and
|
|
40
|
+
`SAHI_SCRIP_MASTER_URL` now override it rather than being required.
|
|
41
|
+
- Tick dicts carry every field, in the shape the REST endpoints return: an
|
|
42
|
+
`optional` field the server did not send is `None` (an index's
|
|
43
|
+
`last_trade_time`, a stock's `open_interest`), an empty order book is `[]`, an
|
|
44
|
+
unset counter is `0`. Previously these keys were missing.
|
|
45
|
+
- Licensed under MIT (previously proprietary).
|
|
46
|
+
- The PyPI page is a short overview that points to https://docs.sahi.com/ for
|
|
47
|
+
guides and the API reference; development notes moved to `CONTRIBUTING.md`.
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
- `SahiFeed` sends a `User-Agent` on the WebSocket upgrade. Production refused
|
|
51
|
+
the handshake without one (HTTP 403), so neither stream connected.
|
|
52
|
+
- A 4xx with no API error envelope (the edge refusing an over-long URL, e.g.
|
|
53
|
+
hundreds of instrument ids in one `get_ltp`) is a plain `SahiAPIException`
|
|
54
|
+
that says the request never reached the API, not a `SahiAuthorisationException`
|
|
55
|
+
with a missing-scope hint.
|
|
56
|
+
|
|
57
|
+
## [0.1.0] - 2026-10-18
|
|
58
|
+
|
|
59
|
+
Pre-launch placeholder; yanked on PyPI. Use 0.2.0 or later.
|
|
60
|
+
|
|
61
|
+
### Added
|
|
62
|
+
- Initial SDK: authentication (`SahiAPI.from_credentials` for the one-call path,
|
|
63
|
+
`get_access_token` + `SahiAPI(token)` for a shared token, rotation via
|
|
64
|
+
`set_access_token`), and full REST coverage — orders, portfolio, funds,
|
|
65
|
+
account, market data (with `get_candle_history`) and the scrip master.
|
|
66
|
+
- `SahiFeed` for the two WebSocket streams: the protobuf tick stream (LTP, quote,
|
|
67
|
+
depth) and JSON order updates, with reconnect, subscription replay and a local
|
|
68
|
+
tick cache whose reads raise the stream's error once it has closed fatally.
|
|
69
|
+
- A typed exception hierarchy mapped from `(status, host, code)`. Every API
|
|
70
|
+
exception (`SahiAPIException`) carries the server `status`, `code`,
|
|
71
|
+
`message`, a `request_id` and an actionable `hint`, all included in `str()`;
|
|
72
|
+
a feed exception carries the WebSocket `close_code` instead.
|
|
73
|
+
- Every wire enum as a constant and `SahiAPI` class attribute.
|
|
74
|
+
- `on_connect` on the order stream for the API's Stream + Snapshot bootstrap;
|
|
75
|
+
transparent reconnect on heartbeat (4408) and slow-consumer (4409) drops.
|
|
76
|
+
- Packaging, CI (ruff, mypy strict, pytest on Python 3.9–3.13) and a
|
|
77
|
+
trusted-publishing release workflow.
|
|
78
|
+
|
|
79
|
+
[Unreleased]: https://github.com/aarityatech/sahi-python/compare/sahiapi/v0.3.0...HEAD
|
|
80
|
+
[0.3.0]: https://github.com/aarityatech/sahi-python/compare/sahiapi/v0.2.0...sahiapi/v0.3.0
|
|
81
|
+
[0.2.0]: https://github.com/aarityatech/sahi-python/compare/sahiapi/v0.1.0...sahiapi/v0.2.0
|
|
82
|
+
[0.1.0]: https://github.com/aarityatech/sahi-python/releases/tag/sahiapi/v0.1.0
|
sahiapi-0.3.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aaritya Technologies Private Limited
|
|
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.
|
sahiapi-0.3.0/PKG-INFO
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sahiapi
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: The official Python client library for the Sahi APIs.
|
|
5
|
+
Project-URL: Homepage, https://docs.sahi.com/
|
|
6
|
+
Project-URL: Documentation, https://docs.sahi.com/sdk/python/
|
|
7
|
+
Author-email: Aaritya Technologies <customercare@sahi.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: bse,market-data,nse,options,sahi,stocks,trading
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: httpx>=0.27
|
|
23
|
+
Requires-Dist: protobuf>=4.25
|
|
24
|
+
Requires-Dist: websocket-client>=1.7
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
27
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
30
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# sahiapi
|
|
34
|
+
|
|
35
|
+
The official Python SDK for the [Sahi trading APIs](https://docs.sahi.com/).
|
|
36
|
+
|
|
37
|
+
Build trading and investing workflows in Python: place and manage orders, track
|
|
38
|
+
holdings, positions and funds, pull quotes, candles and option chains, and
|
|
39
|
+
receive live ticks and order updates over WebSocket.
|
|
40
|
+
|
|
41
|
+
[Sahi](https://www.sahi.com/) © 2026. Released under the [MIT License](https://opensource.org/license/mit).
|
|
42
|
+
|
|
43
|
+
## Documentation
|
|
44
|
+
|
|
45
|
+
- [Python SDK documentation](https://docs.sahi.com/sdk/python/)
|
|
46
|
+
- [Sahi API reference](https://docs.sahi.com/)
|
|
47
|
+
- [Authentication and API keys](https://docs.sahi.com/api/v1/reference/authentication/)
|
|
48
|
+
- [Orders API reference](https://docs.sahi.com/api/v1/reference/orders/)
|
|
49
|
+
|
|
50
|
+
## Features
|
|
51
|
+
|
|
52
|
+
- **Orders**: place, modify and cancel orders, and read the order book and the
|
|
53
|
+
trades behind each order.
|
|
54
|
+
- **Portfolio**: holdings and open positions.
|
|
55
|
+
- **Funds**: available balance and margin in use.
|
|
56
|
+
- **Market data**: LTP and quotes for many instruments in one call, historical
|
|
57
|
+
candles, option expiries and option chains.
|
|
58
|
+
- **Live streams**: `SahiFeed` streams LTP, quotes and market depth, plus
|
|
59
|
+
order updates, and reconnects and resubscribes on its own.
|
|
60
|
+
- **Typed errors**: every API failure raises an exception carrying the HTTP
|
|
61
|
+
status, error code, request id and a hint on how to fix it.
|
|
62
|
+
|
|
63
|
+
## Installation
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install sahiapi
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Requires Python 3.9 or newer.
|
|
70
|
+
|
|
71
|
+
## Quickstart
|
|
72
|
+
|
|
73
|
+
Create an API key in your Sahi account and approve it for the trading day, then
|
|
74
|
+
connect:
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
import os
|
|
78
|
+
from sahiapi import SahiAPI
|
|
79
|
+
|
|
80
|
+
api = SahiAPI.from_credentials(
|
|
81
|
+
key_id=os.environ["SAHI_KEY_ID"],
|
|
82
|
+
secret=os.environ["SAHI_SECRET"],
|
|
83
|
+
)
|
|
84
|
+
print(api.get_profile())
|
|
85
|
+
print(api.get_funds())
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For a TOTP key, also pass the current authenticator code as `totp`. See
|
|
89
|
+
[Mint with a TOTP key](https://docs.sahi.com/sdk/python/authentication/#mint-with-a-totp-key).
|
|
90
|
+
|
|
91
|
+
### Example: place a limit order
|
|
92
|
+
|
|
93
|
+
Prices are in paise, so ₹2,499.55 is `249955`.
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
response = api.place_order(
|
|
97
|
+
exchange_segment=SahiAPI.EXCHANGE_SEGMENT_NSE,
|
|
98
|
+
symbol="RELIANCE",
|
|
99
|
+
txn_type=SahiAPI.TXN_TYPE_BUY,
|
|
100
|
+
qty=1,
|
|
101
|
+
order_type=SahiAPI.ORDER_TYPE_LIMIT,
|
|
102
|
+
product=SahiAPI.PRODUCT_DELIVERY,
|
|
103
|
+
validity=SahiAPI.VALIDITY_DAY,
|
|
104
|
+
price=249955,
|
|
105
|
+
)
|
|
106
|
+
order_id = response["order_ids"][0]
|
|
107
|
+
print(api.get_order_detail(order_id))
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
See the [Python SDK documentation](https://docs.sahi.com/sdk/python/) for streaming, market data and
|
|
111
|
+
every other endpoint.
|
sahiapi-0.3.0/README.md
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# sahiapi
|
|
2
|
+
|
|
3
|
+
The official Python SDK for the [Sahi trading APIs](https://docs.sahi.com/).
|
|
4
|
+
|
|
5
|
+
Build trading and investing workflows in Python: place and manage orders, track
|
|
6
|
+
holdings, positions and funds, pull quotes, candles and option chains, and
|
|
7
|
+
receive live ticks and order updates over WebSocket.
|
|
8
|
+
|
|
9
|
+
[Sahi](https://www.sahi.com/) © 2026. Released under the [MIT License](https://opensource.org/license/mit).
|
|
10
|
+
|
|
11
|
+
## Documentation
|
|
12
|
+
|
|
13
|
+
- [Python SDK documentation](https://docs.sahi.com/sdk/python/)
|
|
14
|
+
- [Sahi API reference](https://docs.sahi.com/)
|
|
15
|
+
- [Authentication and API keys](https://docs.sahi.com/api/v1/reference/authentication/)
|
|
16
|
+
- [Orders API reference](https://docs.sahi.com/api/v1/reference/orders/)
|
|
17
|
+
|
|
18
|
+
## Features
|
|
19
|
+
|
|
20
|
+
- **Orders**: place, modify and cancel orders, and read the order book and the
|
|
21
|
+
trades behind each order.
|
|
22
|
+
- **Portfolio**: holdings and open positions.
|
|
23
|
+
- **Funds**: available balance and margin in use.
|
|
24
|
+
- **Market data**: LTP and quotes for many instruments in one call, historical
|
|
25
|
+
candles, option expiries and option chains.
|
|
26
|
+
- **Live streams**: `SahiFeed` streams LTP, quotes and market depth, plus
|
|
27
|
+
order updates, and reconnects and resubscribes on its own.
|
|
28
|
+
- **Typed errors**: every API failure raises an exception carrying the HTTP
|
|
29
|
+
status, error code, request id and a hint on how to fix it.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install sahiapi
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Requires Python 3.9 or newer.
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
Create an API key in your Sahi account and approve it for the trading day, then
|
|
42
|
+
connect:
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import os
|
|
46
|
+
from sahiapi import SahiAPI
|
|
47
|
+
|
|
48
|
+
api = SahiAPI.from_credentials(
|
|
49
|
+
key_id=os.environ["SAHI_KEY_ID"],
|
|
50
|
+
secret=os.environ["SAHI_SECRET"],
|
|
51
|
+
)
|
|
52
|
+
print(api.get_profile())
|
|
53
|
+
print(api.get_funds())
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
For a TOTP key, also pass the current authenticator code as `totp`. See
|
|
57
|
+
[Mint with a TOTP key](https://docs.sahi.com/sdk/python/authentication/#mint-with-a-totp-key).
|
|
58
|
+
|
|
59
|
+
### Example: place a limit order
|
|
60
|
+
|
|
61
|
+
Prices are in paise, so ₹2,499.55 is `249955`.
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
response = api.place_order(
|
|
65
|
+
exchange_segment=SahiAPI.EXCHANGE_SEGMENT_NSE,
|
|
66
|
+
symbol="RELIANCE",
|
|
67
|
+
txn_type=SahiAPI.TXN_TYPE_BUY,
|
|
68
|
+
qty=1,
|
|
69
|
+
order_type=SahiAPI.ORDER_TYPE_LIMIT,
|
|
70
|
+
product=SahiAPI.PRODUCT_DELIVERY,
|
|
71
|
+
validity=SahiAPI.VALIDITY_DAY,
|
|
72
|
+
price=249955,
|
|
73
|
+
)
|
|
74
|
+
order_id = response["order_ids"][0]
|
|
75
|
+
print(api.get_order_detail(order_id))
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
See the [Python SDK documentation](https://docs.sahi.com/sdk/python/) for streaming, market data and
|
|
79
|
+
every other endpoint.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
|
|
2
|
+
# 1.27 is the first hatchling to write PEP 639 license metadata.
|
|
3
|
+
requires = ["hatchling>=1.27", "hatch-vcs"]
|
|
3
4
|
build-backend = "hatchling.build"
|
|
4
5
|
|
|
5
6
|
[project]
|
|
@@ -10,11 +11,11 @@ dynamic = ["version"]
|
|
|
10
11
|
description = "The official Python client library for the Sahi APIs."
|
|
11
12
|
readme = "README.md"
|
|
12
13
|
requires-python = ">=3.9"
|
|
13
|
-
license =
|
|
14
|
-
|
|
14
|
+
license = "MIT"
|
|
15
|
+
license-files = ["LICENSE"]
|
|
16
|
+
authors = [{ name = "Aaritya Technologies", email = "customercare@sahi.com" }]
|
|
15
17
|
keywords = ["sahi", "trading", "stocks", "options", "market-data", "nse", "bse"]
|
|
16
18
|
classifiers = [
|
|
17
|
-
"Development Status :: 3 - Alpha",
|
|
18
19
|
"Intended Audience :: Developers",
|
|
19
20
|
"Intended Audience :: Financial and Insurance Industry",
|
|
20
21
|
"Operating System :: OS Independent",
|
|
@@ -53,8 +54,7 @@ sahiapi-uat = { path = "tools/sahiapi_uat" }
|
|
|
53
54
|
|
|
54
55
|
[project.urls]
|
|
55
56
|
Homepage = "https://docs.sahi.com/"
|
|
56
|
-
Documentation = "https://docs.sahi.com/"
|
|
57
|
-
Repository = "https://github.com/aarityatech/sahi-python"
|
|
57
|
+
Documentation = "https://docs.sahi.com/sdk/python/"
|
|
58
58
|
|
|
59
59
|
[tool.hatch.version]
|
|
60
60
|
source = "vcs"
|
|
@@ -35,8 +35,10 @@ from .sahi.exceptions import (
|
|
|
35
35
|
SahiFeedForbiddenException,
|
|
36
36
|
SahiFeedNotSubscribedException,
|
|
37
37
|
SahiInvalidOrderStateException,
|
|
38
|
+
SahiInvalidTotpException,
|
|
38
39
|
SahiIpNotAllowedException,
|
|
39
40
|
SahiKeyNotApprovedException,
|
|
41
|
+
SahiMaintenanceException,
|
|
40
42
|
SahiNotFoundException,
|
|
41
43
|
SahiOrderNotFoundException,
|
|
42
44
|
SahiRateLimitException,
|
|
@@ -71,8 +73,10 @@ __all__ = [
|
|
|
71
73
|
"SahiFeedForbiddenException",
|
|
72
74
|
"SahiFeedNotSubscribedException",
|
|
73
75
|
"SahiInvalidOrderStateException",
|
|
76
|
+
"SahiInvalidTotpException",
|
|
74
77
|
"SahiIpNotAllowedException",
|
|
75
78
|
"SahiKeyNotApprovedException",
|
|
79
|
+
"SahiMaintenanceException",
|
|
76
80
|
"SahiNotFoundException",
|
|
77
81
|
"SahiOrderNotFoundException",
|
|
78
82
|
"SahiRateLimitException",
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"""Authentication: minting an access token and reading what it carries.
|
|
2
2
|
|
|
3
|
-
A Sahi access token is minted from an API key
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
A Sahi access token is minted from an API key with :func:`get_access_token`.
|
|
4
|
+
A REGULAR key is approved for the trading day in your Sahi account and then
|
|
5
|
+
exchanges its key id and secret for a token. A TOTP key skips the daily click:
|
|
6
|
+
its six-digit authenticator code goes with the key id and secret, and the first
|
|
7
|
+
correct code of the day is that day's approval, so a strategy can mint
|
|
8
|
+
unattended. The token is a Bearer JWT that every REST and WebSocket call
|
|
9
|
+
carries, and it expires at 06:00 IST — there is no refresh, so mint a fresh one
|
|
10
|
+
each trading day.
|
|
8
11
|
|
|
9
12
|
The helpers here also read a token's own claims without verifying its signature,
|
|
10
13
|
to tell callers when a token expires and what it is allowed to do. They are for
|
|
@@ -40,6 +43,7 @@ def get_access_token(
|
|
|
40
43
|
key_id: str,
|
|
41
44
|
secret: str,
|
|
42
45
|
*,
|
|
46
|
+
totp: str | None = None,
|
|
43
47
|
scopes: list[str] | None = None,
|
|
44
48
|
timeout: float = 10.0,
|
|
45
49
|
) -> dict[str, Any]:
|
|
@@ -51,6 +55,11 @@ def get_access_token(
|
|
|
51
55
|
Args:
|
|
52
56
|
key_id: The key id from your Sahi account.
|
|
53
57
|
secret: The secret shown once when the key was created.
|
|
58
|
+
totp: The current six-digit code for a TOTP key, from the authenticator
|
|
59
|
+
enrolled when the key was created. Required for a TOTP key and
|
|
60
|
+
refused for a REGULAR one. The SDK never holds the TOTP secret:
|
|
61
|
+
generate the code yourself (for example with ``pyotp``) and pass it
|
|
62
|
+
in, so the TOTP secret stays wherever you keep it.
|
|
54
63
|
scopes: Optional subset of the key's scopes to narrow the token to. A
|
|
55
64
|
token can only ever carry scopes the key holds. Omit it, or pass an
|
|
56
65
|
empty list, to get the key's full set.
|
|
@@ -60,20 +69,28 @@ def get_access_token(
|
|
|
60
69
|
The token response dict, unchanged: ``access_token``, ``token_type``,
|
|
61
70
|
``expires_in``, ``expires_at`` and ``scope``.
|
|
62
71
|
|
|
63
|
-
The key id and
|
|
64
|
-
to define, so the SDK does not check it.
|
|
72
|
+
The key id, secret and code are sent as given: their format is the auth
|
|
73
|
+
server's to define, so the SDK does not check it.
|
|
65
74
|
|
|
66
75
|
Raises:
|
|
67
76
|
SahiAuthenticationException: Unknown key id, wrong secret, or a revoked
|
|
68
|
-
key (401
|
|
77
|
+
key (401). The message names a revoked key; the other two read the same.
|
|
78
|
+
SahiInvalidTotpException: The secret was right but the TOTP code was
|
|
79
|
+
not; carries the server's clock for comparison.
|
|
69
80
|
SahiAccountLockedException: The account is frozen or locked.
|
|
70
|
-
SahiKeyNotApprovedException:
|
|
71
|
-
trading day
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
81
|
+
SahiKeyNotApprovedException: A REGULAR key not approved for the current
|
|
82
|
+
trading day, or any key whose account does not have API keys
|
|
83
|
+
enabled yet.
|
|
84
|
+
SahiBadRequestException: A field is malformed, ``scopes`` is not a
|
|
85
|
+
subset of the key's scopes, ``totp`` is missing or not six digits
|
|
86
|
+
for a TOTP key, or ``totp`` was sent for a REGULAR key.
|
|
87
|
+
SahiRateLimitException: Too many mint attempts, or ten wrong TOTP codes
|
|
88
|
+
within a minute; wait for the next minute.
|
|
75
89
|
"""
|
|
76
90
|
body: dict[str, Any] = {"key_id": key_id, "secret": secret}
|
|
91
|
+
# Absent when not given: the server reads a missing, null or empty code the same way.
|
|
92
|
+
if totp is not None:
|
|
93
|
+
body["totp"] = totp
|
|
77
94
|
if scopes is not None:
|
|
78
95
|
body["scopes"] = scopes
|
|
79
96
|
|
|
@@ -95,10 +95,10 @@ class SahiAPI(_Orders, _Portfolio, _Funds, _Account, _MarketData, _Instruments):
|
|
|
95
95
|
token: The response of :func:`sahiapi.get_access_token` (preferred), or
|
|
96
96
|
just its ``access_token`` string. Use :meth:`from_credentials` to go
|
|
97
97
|
straight from an API key to a client.
|
|
98
|
-
scrip_master_url: The scrip-master CSV download URL
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
scrip_master_url: The scrip-master CSV download URL the instrument
|
|
99
|
+
lookups read. Defaults to ``$SAHI_SCRIP_MASTER_URL`` if set, else the
|
|
100
|
+
published file (``env.SCRIP_MASTER_URL``); only another copy of the
|
|
101
|
+
file needs this.
|
|
102
102
|
timeout: Default per-request timeout in seconds.
|
|
103
103
|
|
|
104
104
|
The enum values the API uses are available as class attributes, e.g.
|
|
@@ -194,7 +194,9 @@ class SahiAPI(_Orders, _Portfolio, _Funds, _Account, _MarketData, _Instruments):
|
|
|
194
194
|
self._http = HttpClient(
|
|
195
195
|
env.API_BASE_URL, host=HOST_API, access_token=access_token, timeout=timeout
|
|
196
196
|
)
|
|
197
|
-
self._scrip_master_url =
|
|
197
|
+
self._scrip_master_url = (
|
|
198
|
+
scrip_master_url or os.environ.get(_ENV_SCRIP_MASTER_URL) or env.SCRIP_MASTER_URL
|
|
199
|
+
)
|
|
198
200
|
self._token = state
|
|
199
201
|
|
|
200
202
|
# -- authentication -------------------------------------------------------
|
|
@@ -205,6 +207,7 @@ class SahiAPI(_Orders, _Portfolio, _Funds, _Account, _MarketData, _Instruments):
|
|
|
205
207
|
key_id: str,
|
|
206
208
|
secret: str,
|
|
207
209
|
*,
|
|
210
|
+
totp: str | None = None,
|
|
208
211
|
scopes: list[str] | None = None,
|
|
209
212
|
scrip_master_url: str | None = None,
|
|
210
213
|
timeout: float = 10.0,
|
|
@@ -220,11 +223,14 @@ class SahiAPI(_Orders, _Portfolio, _Funds, _Account, _MarketData, _Instruments):
|
|
|
220
223
|
Args:
|
|
221
224
|
key_id: The key id from your Sahi account.
|
|
222
225
|
secret: The secret shown once at key creation.
|
|
226
|
+
totp: The current six-digit code for a TOTP key; its first correct
|
|
227
|
+
code of the day approves the key, so no dashboard click is
|
|
228
|
+
needed. Omit it for a REGULAR key.
|
|
223
229
|
scopes: Optional subset of the key's scopes to narrow the token to.
|
|
224
|
-
scrip_master_url: Scrip-master CSV URL
|
|
230
|
+
scrip_master_url: Scrip-master CSV URL, as on the constructor.
|
|
225
231
|
timeout: Default per-request timeout in seconds, also used for the mint.
|
|
226
232
|
"""
|
|
227
|
-
token = auth.get_access_token(key_id, secret, scopes=scopes, timeout=timeout)
|
|
233
|
+
token = auth.get_access_token(key_id, secret, totp=totp, scopes=scopes, timeout=timeout)
|
|
228
234
|
return cls(token, scrip_master_url=scrip_master_url, timeout=timeout)
|
|
229
235
|
|
|
230
236
|
def set_access_token(self, token: Token) -> None:
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"""The hosts the SDK talks to.
|
|
2
2
|
|
|
3
3
|
The SDK always talks to the production Sahi hosts. There is no argument or
|
|
4
|
-
environment variable for pointing it anywhere else.
|
|
4
|
+
environment variable for pointing it anywhere else. The scrip master is the one
|
|
5
|
+
exception: it is a static file whose URL the API reference publishes, and
|
|
6
|
+
``SahiAPI(scrip_master_url=...)`` or ``SAHI_SCRIP_MASTER_URL`` can point at
|
|
7
|
+
another copy.
|
|
5
8
|
|
|
6
9
|
The two hosts are separate: the **API** host serves everything under ``/v1``,
|
|
7
10
|
and the **auth** host serves ``/api/v1/apikeys/token``. WebSocket URLs are
|
|
@@ -17,6 +20,7 @@ from __future__ import annotations
|
|
|
17
20
|
__all__ = [
|
|
18
21
|
"API_BASE_URL",
|
|
19
22
|
"AUTH_BASE_URL",
|
|
23
|
+
"SCRIP_MASTER_URL",
|
|
20
24
|
"websocket_base_url",
|
|
21
25
|
]
|
|
22
26
|
|
|
@@ -24,6 +28,8 @@ __all__ = [
|
|
|
24
28
|
API_BASE_URL = "https://direct.sahi.com"
|
|
25
29
|
#: The auth host (serves the token endpoint). No trailing slash.
|
|
26
30
|
AUTH_BASE_URL = "https://apis.sahi.com"
|
|
31
|
+
#: The scrip-master CSV, as published on the API reference's Scrip master page.
|
|
32
|
+
SCRIP_MASTER_URL = "https://assets.sahi.com/external/instruments.csv"
|
|
27
33
|
|
|
28
34
|
|
|
29
35
|
def websocket_base_url(api_base_url: str) -> str:
|
|
@@ -38,8 +38,10 @@ __all__ = [
|
|
|
38
38
|
"SahiFeedForbiddenException",
|
|
39
39
|
"SahiFeedNotSubscribedException",
|
|
40
40
|
"SahiInvalidOrderStateException",
|
|
41
|
+
"SahiInvalidTotpException",
|
|
41
42
|
"SahiIpNotAllowedException",
|
|
42
43
|
"SahiKeyNotApprovedException",
|
|
44
|
+
"SahiMaintenanceException",
|
|
43
45
|
"SahiNotFoundException",
|
|
44
46
|
"SahiOrderNotFoundException",
|
|
45
47
|
"SahiRateLimitException",
|
|
@@ -155,6 +157,46 @@ class SahiAccountLockedException(SahiAuthenticationException):
|
|
|
155
157
|
hint = "The account is locked or frozen. Contact Sahi support to restore access."
|
|
156
158
|
|
|
157
159
|
|
|
160
|
+
class SahiInvalidTotpException(SahiAuthenticationException):
|
|
161
|
+
"""401 ``InvalidTotp``: the key id and secret were right but the TOTP code was not.
|
|
162
|
+
|
|
163
|
+
Raised only for a TOTP key, and only after the secret verified, so the
|
|
164
|
+
credentials themselves are fine. The code was wrong or came from a clock
|
|
165
|
+
more than one step (30 s) away from the server's. Ten wrong codes in a
|
|
166
|
+
minute turn into :class:`SahiRateLimitException`.
|
|
167
|
+
|
|
168
|
+
Attributes:
|
|
169
|
+
server_time: The server's clock at the refusal, as the RFC 3339 string
|
|
170
|
+
it sent, or ``None`` if the envelope carried none. Compare it with
|
|
171
|
+
this host's clock: drift is the usual cause of a code that looks
|
|
172
|
+
right in the authenticator app yet fails here.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
hint = (
|
|
176
|
+
"Mint again with a fresh code from the key's authenticator. If it fails again, "
|
|
177
|
+
"compare this machine's clock with the server_time on the exception."
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def __init__(
|
|
181
|
+
self,
|
|
182
|
+
message: str | None = None,
|
|
183
|
+
*,
|
|
184
|
+
status: int | None = None,
|
|
185
|
+
code: str | None = None,
|
|
186
|
+
request_id: str | None = None,
|
|
187
|
+
server_time: str | None = None,
|
|
188
|
+
) -> None:
|
|
189
|
+
# Set before super().__init__: _render() runs there and reads it.
|
|
190
|
+
self.server_time = server_time
|
|
191
|
+
super().__init__(message, status=status, code=code, request_id=request_id)
|
|
192
|
+
|
|
193
|
+
def _render(self) -> str:
|
|
194
|
+
text = super()._render()
|
|
195
|
+
if self.server_time:
|
|
196
|
+
text = f"{text} [server_time={self.server_time}]"
|
|
197
|
+
return text
|
|
198
|
+
|
|
199
|
+
|
|
158
200
|
class SahiAuthorisationException(SahiAPIException):
|
|
159
201
|
"""403: the token is valid but not permitted to do this.
|
|
160
202
|
|
|
@@ -173,9 +215,19 @@ class SahiKeyNotApprovedException(SahiAuthorisationException):
|
|
|
173
215
|
|
|
174
216
|
Approval is a daily act and lapses at 06:00 IST; it is an exchange
|
|
175
217
|
requirement, not something the SDK can bypass.
|
|
218
|
+
|
|
219
|
+
The auth host also answers 403 when API keys are not enabled for the
|
|
220
|
+
account. That check runs after the secret is verified but before the
|
|
221
|
+
TOTP code is read. So a TOTP key, whose code is its approval, lands here
|
|
222
|
+
only for that reason, and the server message says so; nothing done to the
|
|
223
|
+
key can fix it.
|
|
176
224
|
"""
|
|
177
225
|
|
|
178
|
-
hint =
|
|
226
|
+
hint = (
|
|
227
|
+
"Approve the key for today's trading day in your Sahi account, then mint again. "
|
|
228
|
+
"If the message says API keys are not available for this account, the account "
|
|
229
|
+
"is not enabled yet; contact Sahi."
|
|
230
|
+
)
|
|
179
231
|
|
|
180
232
|
|
|
181
233
|
class SahiIpNotAllowedException(SahiAuthorisationException):
|
|
@@ -256,6 +308,17 @@ class SahiServerException(SahiAPIException):
|
|
|
256
308
|
)
|
|
257
309
|
|
|
258
310
|
|
|
311
|
+
class SahiMaintenanceException(SahiServerException):
|
|
312
|
+
"""503 ``UNDER_MAINTENANCE``: the daily 04:45-07:00 IST maintenance window.
|
|
313
|
+
|
|
314
|
+
The margin endpoints refuse requests while the OMS runs its beginning-of-day
|
|
315
|
+
processing. Nothing is wrong with the request, and a short-wait retry is
|
|
316
|
+
pointless: wait for the window to end.
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
hint = "The OMS is in its daily 04:45-07:00 IST maintenance window. Retry after 07:00 IST."
|
|
320
|
+
|
|
321
|
+
|
|
259
322
|
class SahiTimeoutException(SahiAPIException):
|
|
260
323
|
"""No response arrived within the timeout.
|
|
261
324
|
|
|
@@ -348,6 +411,8 @@ def exception_for(
|
|
|
348
411
|
message: str | None = None,
|
|
349
412
|
request_id: str | None = None,
|
|
350
413
|
retry_after: int | None = None,
|
|
414
|
+
envelope: bool = True,
|
|
415
|
+
server_time: str | None = None,
|
|
351
416
|
) -> SahiAPIException:
|
|
352
417
|
"""Build the right :class:`SahiAPIException` for a non-2xx response.
|
|
353
418
|
|
|
@@ -367,10 +432,26 @@ def exception_for(
|
|
|
367
432
|
message: The envelope ``message``.
|
|
368
433
|
request_id: The request id to attach for support.
|
|
369
434
|
retry_after: Parsed ``Retry-After`` seconds, for 429s.
|
|
435
|
+
envelope: Whether the body was the API's JSON error envelope. A 4xx
|
|
436
|
+
without one never reached the API: the edge (CloudFront's WAF)
|
|
437
|
+
refused it, in practice for an over-long URL. Mapping it by status
|
|
438
|
+
would hand the user a scope or key hint for a problem that is the
|
|
439
|
+
request's size, so it comes back as the base class with a message
|
|
440
|
+
that says where it was refused.
|
|
441
|
+
server_time: The envelope's ``server_time``, which only a wrong TOTP
|
|
442
|
+
code carries; ignored for every other class.
|
|
370
443
|
|
|
371
444
|
Returns:
|
|
372
445
|
An unraised exception instance for the caller to raise.
|
|
373
446
|
"""
|
|
447
|
+
if not envelope and 400 <= status < 500 and status != 429:
|
|
448
|
+
return SahiAPIException(
|
|
449
|
+
"the request was refused before it reached the API (no API error envelope), "
|
|
450
|
+
"usually because the URL is too long; send less per call",
|
|
451
|
+
status=status,
|
|
452
|
+
code=None,
|
|
453
|
+
request_id=request_id,
|
|
454
|
+
)
|
|
374
455
|
key = _normalise(code)
|
|
375
456
|
|
|
376
457
|
def build(cls: type[SahiAPIException]) -> SahiAPIException:
|
|
@@ -387,6 +468,12 @@ def exception_for(
|
|
|
387
468
|
return build(SahiIpNotAllowedException)
|
|
388
469
|
if key == "LOCKED":
|
|
389
470
|
return build(SahiAccountLockedException)
|
|
471
|
+
if key == "UNDERMAINTENANCE":
|
|
472
|
+
return build(SahiMaintenanceException)
|
|
473
|
+
if key == "INVALIDTOTP":
|
|
474
|
+
return SahiInvalidTotpException(
|
|
475
|
+
message, status=status, code=code, request_id=request_id, server_time=server_time
|
|
476
|
+
)
|
|
390
477
|
|
|
391
478
|
if status == 400:
|
|
392
479
|
return build(SahiBadRequestException)
|
|
@@ -8,8 +8,8 @@ Sahi has two streams, and :class:`SahiFeed` owns one connection for each:
|
|
|
8
8
|
your account's order events with no subscription.
|
|
9
9
|
|
|
10
10
|
Both share a token and the connection rules in :mod:`~sahiapi.sahi.ws_client`.
|
|
11
|
-
Ticks update a local cache as they arrive, so :meth:`
|
|
12
|
-
and :meth:`
|
|
11
|
+
Ticks update a local cache as they arrive, so :meth:`last_ltp`, :meth:`last_quote`
|
|
12
|
+
and :meth:`last_depth` return the latest value even before you start consuming.
|
|
13
13
|
Once the tick stream has closed fatally (an expired token above all) those
|
|
14
14
|
reads raise that error instead of serving a frozen price, so a caller that only
|
|
15
15
|
polls the cache learns of it too.
|
|
@@ -194,7 +194,7 @@ class SahiFeed:
|
|
|
194
194
|
the socket thread.
|
|
195
195
|
|
|
196
196
|
The call does not wait for the server. The ids count as subscribed at
|
|
197
|
-
once (:meth:`
|
|
197
|
+
once (:meth:`last_ltp` returns ``None`` until a tick lands), and enter the
|
|
198
198
|
replayed table when the server's ``ack`` arrives. The server refuses a
|
|
199
199
|
request whole if any id is unknown or the per-connection cap would be
|
|
200
200
|
passed; that batch is then dropped, none of its ids are subscribed, and
|
|
@@ -240,7 +240,7 @@ class SahiFeed:
|
|
|
240
240
|
def unsubscribe(self, instrument_ids: list[int]) -> None:
|
|
241
241
|
"""Stop ticks for these instruments and drop their cached values.
|
|
242
242
|
|
|
243
|
-
Afterwards :meth:`
|
|
243
|
+
Afterwards :meth:`last_ltp` / :meth:`last_quote` / :meth:`last_depth` raise
|
|
244
244
|
:class:`SahiFeedNotSubscribedException` for them until re-subscribed.
|
|
245
245
|
"""
|
|
246
246
|
dropped = set(instrument_ids)
|
|
@@ -279,7 +279,7 @@ class SahiFeed:
|
|
|
279
279
|
|
|
280
280
|
# -- cache reads ----------------------------------------------------------
|
|
281
281
|
|
|
282
|
-
def
|
|
282
|
+
def last_ltp(self, instrument_id: int) -> dict[str, Any] | None:
|
|
283
283
|
"""Return the latest LTP tick for an instrument, or ``None`` if none has arrived.
|
|
284
284
|
|
|
285
285
|
Raises:
|
|
@@ -290,7 +290,7 @@ class SahiFeed:
|
|
|
290
290
|
"""
|
|
291
291
|
return self._cached("ltp", instrument_id)
|
|
292
292
|
|
|
293
|
-
def
|
|
293
|
+
def last_quote(self, instrument_id: int) -> dict[str, Any] | None:
|
|
294
294
|
"""Return the latest quote tick for an instrument, or ``None`` if none yet.
|
|
295
295
|
|
|
296
296
|
Raises:
|
|
@@ -301,7 +301,7 @@ class SahiFeed:
|
|
|
301
301
|
"""
|
|
302
302
|
return self._cached("quote", instrument_id)
|
|
303
303
|
|
|
304
|
-
def
|
|
304
|
+
def last_depth(self, instrument_id: int) -> dict[str, Any] | None:
|
|
305
305
|
"""Return the latest depth tick for an instrument, or ``None`` if none yet.
|
|
306
306
|
|
|
307
307
|
Raises:
|
|
@@ -63,10 +63,9 @@ class _Funds(EndpointGroup):
|
|
|
63
63
|
``{"required_margin": ..., "available_margin": ...}``, in paise.
|
|
64
64
|
|
|
65
65
|
Raises:
|
|
66
|
-
|
|
67
|
-
07:00 IST, while the OMS runs its beginning-of-day
|
|
68
|
-
Retry after 07:00 IST
|
|
69
|
-
wait does not apply here.
|
|
66
|
+
SahiMaintenanceException: ``503 UNDER_MAINTENANCE`` between 04:45
|
|
67
|
+
and 07:00 IST, while the OMS runs its beginning-of-day
|
|
68
|
+
processing. Retry after 07:00 IST.
|
|
70
69
|
"""
|
|
71
70
|
body = build_order_body(
|
|
72
71
|
exchange_segment,
|
|
@@ -116,8 +115,8 @@ class _Funds(EndpointGroup):
|
|
|
116
115
|
TypeError: If a leg is missing a required field or carries one the
|
|
117
116
|
API does not define. The message names the leg by zero-based
|
|
118
117
|
index, as the server's own leg errors do.
|
|
119
|
-
|
|
120
|
-
07:00 IST, as on :meth:`get_order_margin`.
|
|
118
|
+
SahiMaintenanceException: ``503 UNDER_MAINTENANCE`` between 04:45
|
|
119
|
+
and 07:00 IST, as on :meth:`get_order_margin`.
|
|
121
120
|
"""
|
|
122
121
|
# The server ignores fields it does not know, so a misspelled key would
|
|
123
122
|
# otherwise be quoted at a value the caller never asked for; building each
|
|
@@ -274,6 +274,8 @@ class HttpClient:
|
|
|
274
274
|
def _error_from(self, response: httpx.Response, request_id: str) -> exceptions.SahiAPIException:
|
|
275
275
|
code: str | None = None
|
|
276
276
|
message: str | None = None
|
|
277
|
+
envelope = True
|
|
278
|
+
server_time: str | None = None
|
|
277
279
|
try:
|
|
278
280
|
body = response.json()
|
|
279
281
|
if isinstance(body, dict):
|
|
@@ -281,7 +283,13 @@ class HttpClient:
|
|
|
281
283
|
# A gateway may send a numeric code; the mapper needs a string.
|
|
282
284
|
code = raw_code if isinstance(raw_code, str) else None
|
|
283
285
|
message = body.get("message")
|
|
286
|
+
# Only a wrong TOTP code carries it; kept verbatim so a caller
|
|
287
|
+
# can compare clocks without the SDK parsing a timestamp.
|
|
288
|
+
raw_time = body.get("server_time")
|
|
289
|
+
server_time = raw_time if isinstance(raw_time, str) else None
|
|
284
290
|
except ValueError:
|
|
291
|
+
# HTML or plain text: the edge answered, not the API.
|
|
292
|
+
envelope = False
|
|
285
293
|
message = response.text or None
|
|
286
294
|
|
|
287
295
|
server_request_id = response.headers.get(_REQUEST_ID_HEADER) or request_id
|
|
@@ -293,6 +301,8 @@ class HttpClient:
|
|
|
293
301
|
message=message,
|
|
294
302
|
request_id=server_request_id,
|
|
295
303
|
retry_after=retry_after,
|
|
304
|
+
envelope=envelope,
|
|
305
|
+
server_time=server_time,
|
|
296
306
|
)
|
|
297
307
|
|
|
298
308
|
@staticmethod
|
|
@@ -21,10 +21,9 @@ One parsed copy is shared by every client in the process, keyed by URL: the file
|
|
|
21
21
|
is around 10 MB and costs well over a hundred megabytes once parsed, and it is
|
|
22
22
|
the same file for every account.
|
|
23
23
|
|
|
24
|
-
The
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
:class:`SahiConfigurationException`.
|
|
24
|
+
The file is downloaded from the URL the API reference's Scrip master page
|
|
25
|
+
publishes (``env.SCRIP_MASTER_URL``). Another copy can be used by passing
|
|
26
|
+
``scrip_master_url`` to :class:`SahiAPI` or setting ``SAHI_SCRIP_MASTER_URL``.
|
|
28
27
|
"""
|
|
29
28
|
|
|
30
29
|
from __future__ import annotations
|
|
@@ -42,7 +41,6 @@ import httpx
|
|
|
42
41
|
from .base import EndpointGroup
|
|
43
42
|
from .exceptions import (
|
|
44
43
|
SahiAPIException,
|
|
45
|
-
SahiConfigurationException,
|
|
46
44
|
SahiConnectionException,
|
|
47
45
|
SahiTimeoutException,
|
|
48
46
|
)
|
|
@@ -100,7 +98,7 @@ class _Instruments(EndpointGroup):
|
|
|
100
98
|
"""
|
|
101
99
|
|
|
102
100
|
# Provided/managed by SahiAPI; declared here so the mixin is self-describing.
|
|
103
|
-
_scrip_master_url: str
|
|
101
|
+
_scrip_master_url: str
|
|
104
102
|
|
|
105
103
|
def get_instruments(
|
|
106
104
|
self, *, refresh: bool = False, timeout: float | None = None
|
|
@@ -130,7 +128,6 @@ class _Instruments(EndpointGroup):
|
|
|
130
128
|
mutating a row changes what later lookups return.
|
|
131
129
|
|
|
132
130
|
Raises:
|
|
133
|
-
SahiConfigurationException: If no scrip-master URL is configured.
|
|
134
131
|
SahiConnectionException: If the download could not be started.
|
|
135
132
|
SahiTimeoutException: If the download times out.
|
|
136
133
|
SahiAPIException: If the download fails (``status`` set for a non-2xx).
|
|
@@ -178,12 +175,6 @@ class _Instruments(EndpointGroup):
|
|
|
178
175
|
|
|
179
176
|
def _master(self, *, refresh: bool, timeout: float | None) -> _Master:
|
|
180
177
|
url = self._scrip_master_url
|
|
181
|
-
if not url:
|
|
182
|
-
raise SahiConfigurationException(
|
|
183
|
-
"no scrip-master URL is configured. Pass scrip_master_url=... to "
|
|
184
|
-
"SahiAPI() or set SAHI_SCRIP_MASTER_URL. The URL is provided when "
|
|
185
|
-
"your API access is set up."
|
|
186
|
-
)
|
|
187
178
|
with _CACHE_LOCK:
|
|
188
179
|
cached = _CACHE.get(url)
|
|
189
180
|
if not refresh and cached is not None and _is_current(cached):
|
|
@@ -22,7 +22,6 @@ from .base import EndpointGroup
|
|
|
22
22
|
|
|
23
23
|
__all__ = ["_MarketData"]
|
|
24
24
|
|
|
25
|
-
_MAX_INSTRUMENTS_PER_REQUEST = 500
|
|
26
25
|
_CANDLE_SIZES = (
|
|
27
26
|
constants.CANDLE_1M,
|
|
28
27
|
constants.CANDLE_3M,
|
|
@@ -43,9 +42,15 @@ class _MarketData(EndpointGroup):
|
|
|
43
42
|
"""Last traded price for one or many instruments (``GET /v1/marketdata/ltp``).
|
|
44
43
|
|
|
45
44
|
Args:
|
|
46
|
-
instrument_ids: One ``instrument_id``, or
|
|
45
|
+
instrument_ids: One ``instrument_id``, or a list of them. The
|
|
47
46
|
endpoint is single or bulk, so a lone id needs no list around it;
|
|
48
|
-
the response is keyed by id either way.
|
|
47
|
+
the response is keyed by id either way. The API accepts up to
|
|
48
|
+
100 ids per call and answers ``400 VALIDATION_FAILED`` above
|
|
49
|
+
that; past roughly 200 the URL outgrows the edge's limit and
|
|
50
|
+
the request is refused before reaching the API (a plain
|
|
51
|
+
:class:`SahiAPIException`, HTTP 403). Batch a longer list. The
|
|
52
|
+
SDK does not cap it: a limit baked into a release would outlive
|
|
53
|
+
a server change.
|
|
49
54
|
|
|
50
55
|
Returns:
|
|
51
56
|
``{"data": {"<id>": {"last_trade_price": ..., "last_trade_time": ...}}}``.
|
|
@@ -53,7 +58,7 @@ class _MarketData(EndpointGroup):
|
|
|
53
58
|
``200``.
|
|
54
59
|
|
|
55
60
|
Raises:
|
|
56
|
-
ValueError: If no ids are given
|
|
61
|
+
ValueError: If no ids are given.
|
|
57
62
|
"""
|
|
58
63
|
ids = self._as_instrument_ids(instrument_ids)
|
|
59
64
|
return self._request( # type: ignore[no-any-return]
|
|
@@ -70,15 +75,15 @@ class _MarketData(EndpointGroup):
|
|
|
70
75
|
"""Full quote with market depth (``GET /v1/marketdata/quotes``).
|
|
71
76
|
|
|
72
77
|
Args:
|
|
73
|
-
instrument_ids: One ``instrument_id``, or
|
|
74
|
-
:meth:`get_ltp
|
|
78
|
+
instrument_ids: One ``instrument_id``, or a list of them, as on
|
|
79
|
+
:meth:`get_ltp` (the API's limit of 100 per call applies here too).
|
|
75
80
|
|
|
76
81
|
Returns:
|
|
77
82
|
``{"data": {"<id>": {...}}}`` — the REST equivalent of the tick
|
|
78
83
|
stream's depth tick: price, volume, DPR band, open interest and depth.
|
|
79
84
|
|
|
80
85
|
Raises:
|
|
81
|
-
ValueError: If no ids are given
|
|
86
|
+
ValueError: If no ids are given.
|
|
82
87
|
"""
|
|
83
88
|
ids = self._as_instrument_ids(instrument_ids)
|
|
84
89
|
return self._request( # type: ignore[no-any-return]
|
|
@@ -243,18 +248,12 @@ class _MarketData(EndpointGroup):
|
|
|
243
248
|
|
|
244
249
|
@staticmethod
|
|
245
250
|
def _as_instrument_ids(instrument_ids: int | Sequence[int]) -> list[int]:
|
|
246
|
-
#
|
|
247
|
-
#
|
|
248
|
-
#
|
|
251
|
+
# Lone id is wrapped, not refused: the endpoint is single-or-bulk.
|
|
252
|
+
# No local id cap on purpose: a limit baked into a released SDK outlives
|
|
253
|
+
# a server change, and the server's 400 names the limit anyway.
|
|
249
254
|
ids = [instrument_ids] if isinstance(instrument_ids, int) else list(instrument_ids)
|
|
250
255
|
if not ids:
|
|
251
256
|
raise ValueError("instrument_ids must contain at least one instrument_id")
|
|
252
|
-
if len(ids) > _MAX_INSTRUMENTS_PER_REQUEST:
|
|
253
|
-
raise ValueError(
|
|
254
|
-
f"at most {_MAX_INSTRUMENTS_PER_REQUEST} instrument_ids per request, got "
|
|
255
|
-
f"{len(ids)}; split the list into batches of "
|
|
256
|
-
f"{_MAX_INSTRUMENTS_PER_REQUEST}"
|
|
257
|
-
)
|
|
258
257
|
return ids
|
|
259
258
|
|
|
260
259
|
@staticmethod
|
|
@@ -11,9 +11,11 @@ The decoded dicts keep the proto field names verbatim (``instrument_id``,
|
|
|
11
11
|
``last_trade_price``), keep integers as integers (unlike protobuf's JSON mapping,
|
|
12
12
|
which renders 64-bit ints as strings), and give enum values as their string names
|
|
13
13
|
with the enum prefix stripped (``NSE``, ``LTP``), matching the constants
|
|
14
|
-
elsewhere in the SDK.
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
elsewhere in the SDK. Every field of the message is present, in the shape the
|
|
15
|
+
REST endpoints return: an ``optional`` the server left unset (an index's
|
|
16
|
+
``last_trade_time``, a stock's ``open_interest``) is ``None``, an empty repeated
|
|
17
|
+
field is ``[]`` and an unset counter is ``0``, so a reader never has to guard
|
|
18
|
+
for a missing key.
|
|
17
19
|
"""
|
|
18
20
|
|
|
19
21
|
from __future__ import annotations
|
|
@@ -111,16 +113,48 @@ def _convert_value(field: FieldDescriptor, value: Any) -> Any:
|
|
|
111
113
|
return value
|
|
112
114
|
|
|
113
115
|
|
|
116
|
+
# Per message type: (name, kind, field) for every field, built once. Decoding
|
|
117
|
+
# runs per tick, and asking the descriptor the same questions each time cost
|
|
118
|
+
# ~40% over the ListFields() loop it replaced.
|
|
119
|
+
_FIELD_PLANS: dict[str, list[tuple[str, str, FieldDescriptor]]] = {}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _field_plan(message: Message) -> list[tuple[str, str, FieldDescriptor]]:
|
|
123
|
+
descriptor = message.DESCRIPTOR
|
|
124
|
+
plan = _FIELD_PLANS.get(descriptor.full_name)
|
|
125
|
+
if plan is None:
|
|
126
|
+
plan = []
|
|
127
|
+
for field in descriptor.fields:
|
|
128
|
+
if _is_repeated(field):
|
|
129
|
+
kind = "repeated"
|
|
130
|
+
elif field.has_presence: # proto3 ``optional`` and sub-messages
|
|
131
|
+
kind = "optional"
|
|
132
|
+
else:
|
|
133
|
+
kind = "scalar"
|
|
134
|
+
plan.append((field.name, kind, field))
|
|
135
|
+
_FIELD_PLANS[descriptor.full_name] = plan
|
|
136
|
+
return plan
|
|
137
|
+
|
|
138
|
+
|
|
114
139
|
def _message_to_dict(message: Message) -> dict[str, Any]:
|
|
115
|
-
"""Convert a protobuf message to a plain dict
|
|
140
|
+
"""Convert a protobuf message to a plain dict with every field present.
|
|
141
|
+
|
|
142
|
+
The wire omits what is zero or empty, so the dict fills it back in to the
|
|
143
|
+
shape the REST endpoints return: an ``optional`` field the server did not
|
|
144
|
+
set is ``None`` (an index has no ``last_trade_time``, a stock no
|
|
145
|
+
``open_interest``), a repeated field with no items is ``[]`` (no order book
|
|
146
|
+
yet), and an unset counter is ``0``. Integers stay integers, enums become
|
|
147
|
+
their stripped string names, nested messages recurse.
|
|
116
148
|
|
|
117
|
-
|
|
118
|
-
|
|
149
|
+
Users key on this shape; dropping a key or changing a default here breaks
|
|
150
|
+
code that reads ``tick["open_interest"]`` on a stock without a guard.
|
|
119
151
|
"""
|
|
120
152
|
result: dict[str, Any] = {}
|
|
121
|
-
for
|
|
122
|
-
if
|
|
123
|
-
result[
|
|
153
|
+
for name, kind, field in _field_plan(message):
|
|
154
|
+
if kind == "repeated":
|
|
155
|
+
result[name] = [_convert_value(field, item) for item in getattr(message, name)]
|
|
156
|
+
elif kind == "optional" and not message.HasField(name):
|
|
157
|
+
result[name] = None
|
|
124
158
|
else:
|
|
125
|
-
result[
|
|
159
|
+
result[name] = _convert_value(field, getattr(message, name))
|
|
126
160
|
return result
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""One WebSocket connection, with reconnect and close-code handling.
|
|
2
2
|
|
|
3
3
|
Both Sahi streams share the same connection rules: the access token goes on the
|
|
4
|
-
upgrade as a Bearer header
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
upgrade as a Bearer header next to the SDK's ``User-Agent`` (production's
|
|
5
|
+
CloudFront refuses an upgrade without one, with a 403), the server pings every
|
|
6
|
+
30 seconds and expects a pong within 15, and some close codes are fatal while
|
|
7
|
+
others just mean reconnect. This class implements those rules once;
|
|
8
|
+
:class:`~sahiapi.sahi.feed.SahiFeed` owns one instance per stream.
|
|
8
9
|
|
|
9
10
|
The connection runs a receive loop on a daemon thread and hands each frame to an
|
|
10
11
|
``on_message`` callback. On a transient drop it reconnects with exponential
|
|
@@ -37,6 +38,7 @@ import threading
|
|
|
37
38
|
from typing import Any, Callable, Protocol
|
|
38
39
|
|
|
39
40
|
from . import constants, exceptions
|
|
41
|
+
from .http import _user_agent
|
|
40
42
|
|
|
41
43
|
__all__ = ["Transport", "WebSocketConnection"]
|
|
42
44
|
|
|
@@ -170,7 +172,7 @@ class WebSocketConnection:
|
|
|
170
172
|
connect_fn: ConnectFn | None = None,
|
|
171
173
|
) -> None:
|
|
172
174
|
self._url = url
|
|
173
|
-
self._header = [f"Authorization: Bearer {access_token}"]
|
|
175
|
+
self._header = [f"Authorization: Bearer {access_token}", f"User-Agent: {_user_agent()}"]
|
|
174
176
|
self._binary = binary
|
|
175
177
|
self._on_message = on_message
|
|
176
178
|
self._on_open = on_open
|
sahiapi-0.1.0/CHANGELOG.md
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
All notable changes to this project are documented here. The format follows
|
|
4
|
-
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project aims to
|
|
5
|
-
follow [Semantic Versioning](https://semver.org/) once it reaches `1.0.0`.
|
|
6
|
-
|
|
7
|
-
## [Unreleased]
|
|
8
|
-
|
|
9
|
-
## [0.1.0] - 2026-10-18
|
|
10
|
-
|
|
11
|
-
### Added
|
|
12
|
-
- Initial SDK: authentication (`SahiAPI.from_credentials` for the one-call path,
|
|
13
|
-
`get_access_token` + `SahiAPI(token)` for a shared token, rotation via
|
|
14
|
-
`set_access_token`), and full REST coverage — orders, portfolio, funds,
|
|
15
|
-
account, market data (with `get_candle_history`) and the scrip master.
|
|
16
|
-
- `SahiFeed` for the two WebSocket streams: the protobuf tick stream (LTP, quote,
|
|
17
|
-
depth) and JSON order updates, with reconnect, subscription replay and a local
|
|
18
|
-
tick cache whose reads raise the stream's error once it has closed fatally.
|
|
19
|
-
- A typed exception hierarchy mapped from `(status, host, code)`. Every API
|
|
20
|
-
exception (`SahiAPIException`) carries the server `status`, `code`,
|
|
21
|
-
`message`, a `request_id` and an actionable `hint`, all included in `str()`;
|
|
22
|
-
a feed exception carries the WebSocket `close_code` instead.
|
|
23
|
-
- Every wire enum as a constant and `SahiAPI` class attribute.
|
|
24
|
-
- `on_connect` on the order stream for the API's Stream + Snapshot bootstrap;
|
|
25
|
-
transparent reconnect on heartbeat (4408) and slow-consumer (4409) drops.
|
|
26
|
-
- Packaging, CI (ruff, mypy strict, pytest on Python 3.9–3.13) and a
|
|
27
|
-
trusted-publishing release workflow.
|
|
28
|
-
|
|
29
|
-
[Unreleased]: https://github.com/aarityatech/sahi-python/compare/sahiapi/v0.1.0...HEAD
|
|
30
|
-
[0.1.0]: https://github.com/aarityatech/sahi-python/releases/tag/sahiapi/v0.1.0
|
sahiapi-0.1.0/LICENSE
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
Copyright (c) 2026 Aaritya Technologies Private Limited. All rights reserved.
|
|
2
|
-
|
|
3
|
-
This software and its source code are proprietary and confidential. Until an
|
|
4
|
-
open-source license is applied by the copyright holder, no permission is granted
|
|
5
|
-
to copy, use, modify, or distribute this software outside Aaritya Technologies.
|
|
6
|
-
|
|
7
|
-
An open-source license will replace this file when the project is published.
|
sahiapi-0.1.0/PKG-INFO
DELETED
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.5
|
|
2
|
-
Name: sahiapi
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: The official Python client library for the Sahi APIs.
|
|
5
|
-
Project-URL: Homepage, https://docs.sahi.com/
|
|
6
|
-
Project-URL: Documentation, https://docs.sahi.com/
|
|
7
|
-
Project-URL: Repository, https://github.com/aarityatech/sahi-python
|
|
8
|
-
Author: Aaritya Technologies
|
|
9
|
-
License: Copyright (c) 2026 Aaritya Technologies Private Limited. All rights reserved.
|
|
10
|
-
|
|
11
|
-
This software and its source code are proprietary and confidential. Until an
|
|
12
|
-
open-source license is applied by the copyright holder, no permission is granted
|
|
13
|
-
to copy, use, modify, or distribute this software outside Aaritya Technologies.
|
|
14
|
-
|
|
15
|
-
An open-source license will replace this file when the project is published.
|
|
16
|
-
License-File: LICENSE
|
|
17
|
-
Keywords: bse,market-data,nse,options,sahi,stocks,trading
|
|
18
|
-
Classifier: Development Status :: 3 - Alpha
|
|
19
|
-
Classifier: Intended Audience :: Developers
|
|
20
|
-
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
21
|
-
Classifier: Operating System :: OS Independent
|
|
22
|
-
Classifier: Programming Language :: Python :: 3
|
|
23
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
24
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
25
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
26
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
27
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
28
|
-
Classifier: Typing :: Typed
|
|
29
|
-
Requires-Python: >=3.9
|
|
30
|
-
Requires-Dist: httpx>=0.27
|
|
31
|
-
Requires-Dist: protobuf>=4.25
|
|
32
|
-
Requires-Dist: websocket-client>=1.7
|
|
33
|
-
Provides-Extra: dev
|
|
34
|
-
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
35
|
-
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
36
|
-
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
37
|
-
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
38
|
-
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
39
|
-
Description-Content-Type: text/markdown
|
|
40
|
-
|
|
41
|
-
# sahiapi
|
|
42
|
-
|
|
43
|
-
The official Python client library for the [Sahi APIs](https://docs.sahi.com/):
|
|
44
|
-
orders, portfolio, funds, market data and live streams.
|
|
45
|
-
|
|
46
|
-
> Early development. The public interface is not yet stable.
|
|
47
|
-
|
|
48
|
-
## Install
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
pip install sahiapi
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Requires Python 3.9 or newer.
|
|
55
|
-
|
|
56
|
-
## Quickstart
|
|
57
|
-
|
|
58
|
-
Create an API key in your Sahi account and approve it for the trading day, then:
|
|
59
|
-
|
|
60
|
-
```python
|
|
61
|
-
import os
|
|
62
|
-
from sahiapi import SahiAPI
|
|
63
|
-
|
|
64
|
-
api = SahiAPI.from_credentials(
|
|
65
|
-
key_id=os.environ["SAHI_KEY_ID"],
|
|
66
|
-
secret=os.environ["SAHI_SECRET"],
|
|
67
|
-
)
|
|
68
|
-
print(api.get_profile())
|
|
69
|
-
print(api.get_funds())
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Tokens expire at 06:00 IST, so mint a fresh one each trading day.
|
|
73
|
-
|
|
74
|
-
## Documentation
|
|
75
|
-
|
|
76
|
-
Guides, the full API reference and examples are at **https://docs.sahi.com/**.
|
|
77
|
-
|
|
78
|
-
The SDK returns the API's JSON exactly as documented there: money is integer
|
|
79
|
-
paise, timestamps are epoch milliseconds, and field names are unchanged.
|
|
80
|
-
|
|
81
|
-
## Development
|
|
82
|
-
|
|
83
|
-
```bash
|
|
84
|
-
uv sync --extra dev
|
|
85
|
-
uv run pytest
|
|
86
|
-
uv run ruff check .
|
|
87
|
-
uv run mypy
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
The tick-stream protobuf bindings are generated and committed; regenerate them
|
|
91
|
-
with `tools/regen_proto.sh` after editing the `.proto`. Releases are cut with
|
|
92
|
-
`tools/release.sh`; see `.github/workflows/release.yml`.
|
|
93
|
-
|
|
94
|
-
## License
|
|
95
|
-
|
|
96
|
-
Proprietary during development; an open-source license will be applied before
|
|
97
|
-
public release. See [LICENSE](LICENSE).
|
sahiapi-0.1.0/README.md
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# sahiapi
|
|
2
|
-
|
|
3
|
-
The official Python client library for the [Sahi APIs](https://docs.sahi.com/):
|
|
4
|
-
orders, portfolio, funds, market data and live streams.
|
|
5
|
-
|
|
6
|
-
> Early development. The public interface is not yet stable.
|
|
7
|
-
|
|
8
|
-
## Install
|
|
9
|
-
|
|
10
|
-
```bash
|
|
11
|
-
pip install sahiapi
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
Requires Python 3.9 or newer.
|
|
15
|
-
|
|
16
|
-
## Quickstart
|
|
17
|
-
|
|
18
|
-
Create an API key in your Sahi account and approve it for the trading day, then:
|
|
19
|
-
|
|
20
|
-
```python
|
|
21
|
-
import os
|
|
22
|
-
from sahiapi import SahiAPI
|
|
23
|
-
|
|
24
|
-
api = SahiAPI.from_credentials(
|
|
25
|
-
key_id=os.environ["SAHI_KEY_ID"],
|
|
26
|
-
secret=os.environ["SAHI_SECRET"],
|
|
27
|
-
)
|
|
28
|
-
print(api.get_profile())
|
|
29
|
-
print(api.get_funds())
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
Tokens expire at 06:00 IST, so mint a fresh one each trading day.
|
|
33
|
-
|
|
34
|
-
## Documentation
|
|
35
|
-
|
|
36
|
-
Guides, the full API reference and examples are at **https://docs.sahi.com/**.
|
|
37
|
-
|
|
38
|
-
The SDK returns the API's JSON exactly as documented there: money is integer
|
|
39
|
-
paise, timestamps are epoch milliseconds, and field names are unchanged.
|
|
40
|
-
|
|
41
|
-
## Development
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
uv sync --extra dev
|
|
45
|
-
uv run pytest
|
|
46
|
-
uv run ruff check .
|
|
47
|
-
uv run mypy
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
The tick-stream protobuf bindings are generated and committed; regenerate them
|
|
51
|
-
with `tools/regen_proto.sh` after editing the `.proto`. Releases are cut with
|
|
52
|
-
`tools/release.sh`; see `.github/workflows/release.yml`.
|
|
53
|
-
|
|
54
|
-
## License
|
|
55
|
-
|
|
56
|
-
Proprietary during development; an open-source license will be applied before
|
|
57
|
-
public release. See [LICENSE](LICENSE).
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|