sahiapi 0.1.0__tar.gz → 0.2.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. sahiapi-0.2.0/CHANGELOG.md +69 -0
  2. sahiapi-0.2.0/LICENSE +21 -0
  3. sahiapi-0.2.0/PKG-INFO +108 -0
  4. sahiapi-0.2.0/README.md +76 -0
  5. {sahiapi-0.1.0 → sahiapi-0.2.0}/pyproject.toml +6 -6
  6. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/__init__.py +2 -0
  7. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/client.py +8 -6
  8. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/env.py +7 -1
  9. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/exceptions.py +29 -0
  10. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/feed.py +7 -7
  11. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/funds.py +5 -6
  12. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/http.py +4 -0
  13. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/instruments.py +4 -13
  14. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/marketdata.py +15 -16
  15. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/proto/codec.py +44 -10
  16. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/ws_client.py +7 -5
  17. sahiapi-0.1.0/CHANGELOG.md +0 -30
  18. sahiapi-0.1.0/LICENSE +0 -7
  19. sahiapi-0.1.0/PKG-INFO +0 -97
  20. sahiapi-0.1.0/README.md +0 -57
  21. {sahiapi-0.1.0 → sahiapi-0.2.0}/.gitignore +0 -0
  22. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/py.typed +0 -0
  23. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/__init__.py +0 -0
  24. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/account.py +0 -0
  25. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/auth.py +0 -0
  26. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/base.py +0 -0
  27. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/constants.py +0 -0
  28. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/orders.py +0 -0
  29. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/portfolio.py +0 -0
  30. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/proto/__init__.py +0 -0
  31. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/proto/marketdata.proto +0 -0
  32. {sahiapi-0.1.0 → sahiapi-0.2.0}/src/sahiapi/sahi/proto/marketdata_pb2.py +0 -0
@@ -0,0 +1,69 @@
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.2.0] - 2026-10-22
10
+
11
+ First public release. Includes everything listed under 0.1.0.
12
+
13
+ ### Added
14
+ - `SahiMaintenanceException` (a `SahiServerException`) for `503 UNDER_MAINTENANCE`,
15
+ the margin endpoints' daily 04:45–07:00 IST window, with a hint that says so
16
+ instead of "retry after a short wait".
17
+
18
+ ### Changed
19
+ - `SahiFeed` cache reads are `last_ltp`, `last_quote` and `last_depth` (were
20
+ `get_ltp`, `get_quote`, `get_depth`), so they no longer share a name with the
21
+ REST calls `SahiAPI.get_ltp` / `get_quotes` that have a different signature.
22
+ - `get_ltp` / `get_quotes` no longer cap the id list locally. The SDK said 500
23
+ while the API allows 100, which is the trouble with a limit baked into a
24
+ release: the server's `400 VALIDATION_FAILED` names the limit, and the
25
+ docstrings state it.
26
+ - The scrip master downloads from the published URL by default
27
+ (`https://assets.sahi.com/external/instruments.csv`); `scrip_master_url=` and
28
+ `SAHI_SCRIP_MASTER_URL` now override it rather than being required.
29
+ - Tick dicts carry every field, in the shape the REST endpoints return: an
30
+ `optional` field the server did not send is `None` (an index's
31
+ `last_trade_time`, a stock's `open_interest`), an empty order book is `[]`, an
32
+ unset counter is `0`. Previously these keys were missing.
33
+ - Licensed under MIT (previously proprietary).
34
+ - The PyPI page is a short overview that points to https://docs.sahi.com/ for
35
+ guides and the API reference; development notes moved to `CONTRIBUTING.md`.
36
+
37
+ ### Fixed
38
+ - `SahiFeed` sends a `User-Agent` on the WebSocket upgrade. Production refused
39
+ the handshake without one (HTTP 403), so neither stream connected.
40
+ - A 4xx with no API error envelope (the edge refusing an over-long URL, e.g.
41
+ hundreds of instrument ids in one `get_ltp`) is a plain `SahiAPIException`
42
+ that says the request never reached the API, not a `SahiAuthorisationException`
43
+ with a missing-scope hint.
44
+
45
+ ## [0.1.0] - 2026-10-18
46
+
47
+ Pre-launch placeholder; yanked on PyPI. Use 0.2.0 or later.
48
+
49
+ ### Added
50
+ - Initial SDK: authentication (`SahiAPI.from_credentials` for the one-call path,
51
+ `get_access_token` + `SahiAPI(token)` for a shared token, rotation via
52
+ `set_access_token`), and full REST coverage — orders, portfolio, funds,
53
+ account, market data (with `get_candle_history`) and the scrip master.
54
+ - `SahiFeed` for the two WebSocket streams: the protobuf tick stream (LTP, quote,
55
+ depth) and JSON order updates, with reconnect, subscription replay and a local
56
+ tick cache whose reads raise the stream's error once it has closed fatally.
57
+ - A typed exception hierarchy mapped from `(status, host, code)`. Every API
58
+ exception (`SahiAPIException`) carries the server `status`, `code`,
59
+ `message`, a `request_id` and an actionable `hint`, all included in `str()`;
60
+ a feed exception carries the WebSocket `close_code` instead.
61
+ - Every wire enum as a constant and `SahiAPI` class attribute.
62
+ - `on_connect` on the order stream for the API's Stream + Snapshot bootstrap;
63
+ transparent reconnect on heartbeat (4408) and slow-consumer (4409) drops.
64
+ - Packaging, CI (ruff, mypy strict, pytest on Python 3.9–3.13) and a
65
+ trusted-publishing release workflow.
66
+
67
+ [Unreleased]: https://github.com/aarityatech/sahi-python/compare/sahiapi/v0.2.0...HEAD
68
+ [0.2.0]: https://github.com/aarityatech/sahi-python/compare/sahiapi/v0.1.0...sahiapi/v0.2.0
69
+ [0.1.0]: https://github.com/aarityatech/sahi-python/releases/tag/sahiapi/v0.1.0
sahiapi-0.2.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.2.0/PKG-INFO ADDED
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.5
2
+ Name: sahiapi
3
+ Version: 0.2.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
+ ### Example: place a limit order
89
+
90
+ Prices are in paise, so ₹2,499.55 is `249955`.
91
+
92
+ ```python
93
+ response = api.place_order(
94
+ exchange_segment=SahiAPI.EXCHANGE_SEGMENT_NSE,
95
+ symbol="RELIANCE",
96
+ txn_type=SahiAPI.TXN_TYPE_BUY,
97
+ qty=1,
98
+ order_type=SahiAPI.ORDER_TYPE_LIMIT,
99
+ product=SahiAPI.PRODUCT_DELIVERY,
100
+ validity=SahiAPI.VALIDITY_DAY,
101
+ price=249955,
102
+ )
103
+ order_id = response["order_ids"][0]
104
+ print(api.get_order_detail(order_id))
105
+ ```
106
+
107
+ See the [Python SDK documentation](https://docs.sahi.com/sdk/python/) for streaming, market data and
108
+ every other endpoint.
@@ -0,0 +1,76 @@
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
+ ### Example: place a limit order
57
+
58
+ Prices are in paise, so ₹2,499.55 is `249955`.
59
+
60
+ ```python
61
+ response = api.place_order(
62
+ exchange_segment=SahiAPI.EXCHANGE_SEGMENT_NSE,
63
+ symbol="RELIANCE",
64
+ txn_type=SahiAPI.TXN_TYPE_BUY,
65
+ qty=1,
66
+ order_type=SahiAPI.ORDER_TYPE_LIMIT,
67
+ product=SahiAPI.PRODUCT_DELIVERY,
68
+ validity=SahiAPI.VALIDITY_DAY,
69
+ price=249955,
70
+ )
71
+ order_id = response["order_ids"][0]
72
+ print(api.get_order_detail(order_id))
73
+ ```
74
+
75
+ See the [Python SDK documentation](https://docs.sahi.com/sdk/python/) for streaming, market data and
76
+ every other endpoint.
@@ -1,5 +1,6 @@
1
1
  [build-system]
2
- requires = ["hatchling", "hatch-vcs"]
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 = { file = "LICENSE" }
14
- authors = [{ name = "Aaritya Technologies" }]
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"
@@ -37,6 +37,7 @@ from .sahi.exceptions import (
37
37
  SahiInvalidOrderStateException,
38
38
  SahiIpNotAllowedException,
39
39
  SahiKeyNotApprovedException,
40
+ SahiMaintenanceException,
40
41
  SahiNotFoundException,
41
42
  SahiOrderNotFoundException,
42
43
  SahiRateLimitException,
@@ -73,6 +74,7 @@ __all__ = [
73
74
  "SahiInvalidOrderStateException",
74
75
  "SahiIpNotAllowedException",
75
76
  "SahiKeyNotApprovedException",
77
+ "SahiMaintenanceException",
76
78
  "SahiNotFoundException",
77
79
  "SahiOrderNotFoundException",
78
80
  "SahiRateLimitException",
@@ -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, needed for the
99
- instrument lookups. Defaults to ``$SAHI_SCRIP_MASTER_URL``. The URL
100
- is not in the public docs; it is provided when your API access is set
101
- up.
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 = scrip_master_url or os.environ.get(_ENV_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 -------------------------------------------------------
@@ -221,7 +223,7 @@ class SahiAPI(_Orders, _Portfolio, _Funds, _Account, _MarketData, _Instruments):
221
223
  key_id: The key id from your Sahi account.
222
224
  secret: The secret shown once at key creation.
223
225
  scopes: Optional subset of the key's scopes to narrow the token to.
224
- scrip_master_url: Scrip-master CSV URL (default ``$SAHI_SCRIP_MASTER_URL``).
226
+ scrip_master_url: Scrip-master CSV URL, as on the constructor.
225
227
  timeout: Default per-request timeout in seconds, also used for the mint.
226
228
  """
227
229
  token = auth.get_access_token(key_id, secret, scopes=scopes, timeout=timeout)
@@ -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:
@@ -40,6 +40,7 @@ __all__ = [
40
40
  "SahiInvalidOrderStateException",
41
41
  "SahiIpNotAllowedException",
42
42
  "SahiKeyNotApprovedException",
43
+ "SahiMaintenanceException",
43
44
  "SahiNotFoundException",
44
45
  "SahiOrderNotFoundException",
45
46
  "SahiRateLimitException",
@@ -256,6 +257,17 @@ class SahiServerException(SahiAPIException):
256
257
  )
257
258
 
258
259
 
260
+ class SahiMaintenanceException(SahiServerException):
261
+ """503 ``UNDER_MAINTENANCE``: the daily 04:45-07:00 IST maintenance window.
262
+
263
+ The margin endpoints refuse requests while the OMS runs its beginning-of-day
264
+ processing. Nothing is wrong with the request, and a short-wait retry is
265
+ pointless: wait for the window to end.
266
+ """
267
+
268
+ hint = "The OMS is in its daily 04:45-07:00 IST maintenance window. Retry after 07:00 IST."
269
+
270
+
259
271
  class SahiTimeoutException(SahiAPIException):
260
272
  """No response arrived within the timeout.
261
273
 
@@ -348,6 +360,7 @@ def exception_for(
348
360
  message: str | None = None,
349
361
  request_id: str | None = None,
350
362
  retry_after: int | None = None,
363
+ envelope: bool = True,
351
364
  ) -> SahiAPIException:
352
365
  """Build the right :class:`SahiAPIException` for a non-2xx response.
353
366
 
@@ -367,10 +380,24 @@ def exception_for(
367
380
  message: The envelope ``message``.
368
381
  request_id: The request id to attach for support.
369
382
  retry_after: Parsed ``Retry-After`` seconds, for 429s.
383
+ envelope: Whether the body was the API's JSON error envelope. A 4xx
384
+ without one never reached the API: the edge (CloudFront's WAF)
385
+ refused it, in practice for an over-long URL. Mapping it by status
386
+ would hand the user a scope or key hint for a problem that is the
387
+ request's size, so it comes back as the base class with a message
388
+ that says where it was refused.
370
389
 
371
390
  Returns:
372
391
  An unraised exception instance for the caller to raise.
373
392
  """
393
+ if not envelope and 400 <= status < 500 and status != 429:
394
+ return SahiAPIException(
395
+ "the request was refused before it reached the API (no API error envelope), "
396
+ "usually because the URL is too long; send less per call",
397
+ status=status,
398
+ code=None,
399
+ request_id=request_id,
400
+ )
374
401
  key = _normalise(code)
375
402
 
376
403
  def build(cls: type[SahiAPIException]) -> SahiAPIException:
@@ -387,6 +414,8 @@ def exception_for(
387
414
  return build(SahiIpNotAllowedException)
388
415
  if key == "LOCKED":
389
416
  return build(SahiAccountLockedException)
417
+ if key == "UNDERMAINTENANCE":
418
+ return build(SahiMaintenanceException)
390
419
 
391
420
  if status == 400:
392
421
  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:`get_ltp`, :meth:`get_quote`
12
- and :meth:`get_depth` return the latest value even before you start consuming.
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:`get_ltp` returns ``None`` until a tick lands), and enter the
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:`get_ltp` / :meth:`get_quote` / :meth:`get_depth` raise
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 get_ltp(self, instrument_id: int) -> dict[str, Any] | None:
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 get_quote(self, instrument_id: int) -> dict[str, Any] | None:
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 get_depth(self, instrument_id: int) -> dict[str, Any] | None:
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
- SahiServerException: ``503 UNDER_MAINTENANCE`` between 04:45 and
67
- 07:00 IST, while the OMS runs its beginning-of-day processing.
68
- Retry after 07:00 IST; the exception's generic hint of a short
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
- SahiServerException: ``503 UNDER_MAINTENANCE`` between 04:45 and
120
- 07:00 IST, as on :meth:`get_order_margin`. Retry after 07:00 IST.
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,7 @@ 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
277
278
  try:
278
279
  body = response.json()
279
280
  if isinstance(body, dict):
@@ -282,6 +283,8 @@ class HttpClient:
282
283
  code = raw_code if isinstance(raw_code, str) else None
283
284
  message = body.get("message")
284
285
  except ValueError:
286
+ # HTML or plain text: the edge answered, not the API.
287
+ envelope = False
285
288
  message = response.text or None
286
289
 
287
290
  server_request_id = response.headers.get(_REQUEST_ID_HEADER) or request_id
@@ -293,6 +296,7 @@ class HttpClient:
293
296
  message=message,
294
297
  request_id=server_request_id,
295
298
  retry_after=retry_after,
299
+ envelope=envelope,
296
300
  )
297
301
 
298
302
  @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 download URL is not part of the public API reference; it is provided when
25
- your API access is set up. Pass it as ``scrip_master_url`` to :class:`SahiAPI`, or
26
- set ``SAHI_SCRIP_MASTER_URL``. Until one is given, these methods raise
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 | None
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 up to 500 of them. The
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, or more than 500 are.
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 up to 500 of them, as on
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, or more than 500 are.
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
- # The endpoint is single or bulk, so a lone id is wrapped rather than
247
- # refused. Both limits are the API's own rules; failing here saves a round
248
- # trip that would come back 400 and gives a message that names the fix.
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. Only fields the frame actually carries appear, so an absent
15
- optional such as an index's ``last_trade_time`` is simply missing rather than a
16
- zero.
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, set fields only.
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
- Integers stay integers, enums become their stripped string names, nested
118
- messages recurse, and repeated fields become lists.
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 field, value in message.ListFields():
122
- if _is_repeated(field):
123
- result[field.name] = [_convert_value(field, item) for item in value]
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[field.name] = _convert_value(field, value)
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, the server pings every 30 seconds and expects a pong
5
- within 15, and some close codes are fatal while others just mean reconnect. This
6
- class implements those rules once; :class:`~sahiapi.sahi.feed.SahiFeed` owns one
7
- instance per stream.
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
@@ -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